Skip to content

Commit 8eb87ba

Browse files
zombieJclaude
andauthored
fix: Skip first frame render with motion appear (#73)
* refactor: add NONE state for styleReady to distinguish initial mount On initial mount when status is STATUS_NONE, return 'NONE' instead of true to prevent rendering children until style is ready. This improves the appear animation behavior by ensuring style synchronization. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: update styleReady test to use correct className assertion Removed TODO and updated className assertion to verify the first render (prepare stage) with correct className value. Also changed from toHaveBeenCalled to checking mockRender.mock.calls[0][0] to match the first render call. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve TypeScript error for jest.mock.calls access --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 634cdf2 commit 8eb87ba

File tree

3 files changed

+31
-2
lines changed

3 files changed

+31
-2
lines changed

src/CSSMotion.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,10 @@ export function genCSSMotion(config: CSSMotionConfig) {
190190

191191
// We should render children when motionStyle is sync with stepStatus
192192
return React.useMemo(() => {
193+
if (styleReady === 'NONE') {
194+
return null;
195+
}
196+
193197
let motionChildren: React.ReactNode;
194198
const mergedProps = { ...eventProps, visible };
195199

src/hooks/useStatus.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export default function useStatus(
5353
stepStatus: StepStatus,
5454
style: React.CSSProperties,
5555
visible: boolean,
56-
styleReady: boolean,
56+
styleReady: 'NONE' | boolean,
5757
] {
5858
// Used for outer render usage to avoid `visible: false & status: none` to render nothing
5959
const [asyncVisible, setAsyncVisible] = React.useState<boolean>();
@@ -311,6 +311,13 @@ export default function useStatus(
311311
step,
312312
mergedStyle,
313313
asyncVisible ?? visible,
314-
step === STEP_START || step === STEP_ACTIVE ? styleStep === step : true,
314+
315+
!mountedRef.current && currentStatus === STATUS_NONE
316+
? // Appear
317+
'NONE'
318+
: // Enter or Leave
319+
step === STEP_START || step === STEP_ACTIVE
320+
? styleStep === step
321+
: true,
315322
];
316323
}

tests/CSSMotion.spec.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,24 @@ describe('CSSMotion', () => {
492492
expect(activeBoxNode).toHaveClass(`animation-leave-active`);
493493
});
494494

495+
it('styleReady returns NONE on first mount when status is STATUS_NONE', () => {
496+
const mockRender = jest.fn(() => null) as jest.Mock;
497+
(mockRender as any).mock.calls = [] as any;
498+
499+
render(
500+
<CSSMotion visible motionAppear motionName="test">
501+
{mockRender}
502+
</CSSMotion>,
503+
);
504+
505+
// First render (prepare stage)
506+
expect(mockRender.mock.calls[0][0]).toEqual(
507+
expect.objectContaining({
508+
className: 'test-appear test-appear-prepare test',
509+
}),
510+
);
511+
});
512+
495513
describe('immediately', () => {
496514
it('motionLeaveImmediately', async () => {
497515
const { container } = render(

0 commit comments

Comments
 (0)