Skip to content

Conversation

@zombieJ
Copy link
Member

@zombieJ zombieJ commented Feb 11, 2026

修复部分机型下, useLayoutEffect 仍会多渲染一帧的问题。

ref ant-design/ant-design#56604

PS: 需要到 antd 里也抬一下。

Summary by CodeRabbit

发布说明

  • 改进
    • 优化动画组件渲染时机:在样式未准备好时阻止渲染,避免初始闪烁或不一致的过渡行为。
  • 测试
    • 新增测试覆盖,验证初次挂载时的样式就绪信号,确保行为稳定并防止回归。

zombieJ and others added 2 commits February 11, 2026 14:46
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>
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>
@vercel
Copy link

vercel bot commented Feb 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
motion Ready Ready Preview, Comment Feb 11, 2026 7:03am

@gemini-code-assist
Copy link

Summary of Changes

Hello @zombieJ, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求旨在优化组件的初始渲染行为,特别是在使用动画入场(motion appear)时。通过引入一个特殊的渲染就绪状态,它能够有效避免在某些设备上因 useLayoutEffect 导致的额外首帧渲染,从而提升用户体验并减少潜在的视觉闪烁。

Highlights

  • 修复首帧渲染问题: 解决了在 motion appear 场景下,useLayoutEffect 仍可能导致额外首帧渲染的问题。
  • 引入 'NONE' 状态: 为 styleReady 引入了新的 'NONE' 状态,用于明确指示在特定条件下跳过组件的首次渲染。
  • 优化渲染逻辑: CSSMotion 组件在 styleReady'NONE' 时将直接返回 null,从而避免不必要的渲染。
  • 更新 useStatus Hook: 调整了 useStatus Hook 的逻辑,使其在组件初次挂载且状态为 STATUS_NONE 时,将 styleReady 设置为 'NONE'
  • 新增测试用例: 增加了新的测试用例,以验证 styleReady 在初次挂载时返回 'NONE' 的行为。

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/CSSMotion.tsx
    • React.useMemo 中添加了对 styleReady === 'NONE' 的检查,如果为真则返回 null
  • src/hooks/useStatus.ts
    • styleReady 参数的类型从 boolean 更改为 'NONE' | boolean
    • 修改了 styleReady 的计算逻辑,使其在组件未挂载且当前状态为 STATUS_NONE 时返回 'NONE'
  • tests/CSSMotion.spec.tsx
    • 新增了一个测试用例,验证在 motionAppear 且初次挂载时,styleReady 会返回 'NONE'
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

Walkthrough

在 CSSMotion 的渲染路径中新增对 styleReady 为 'NONE' 的早期返回,配合将 useStatusstyleReady 类型从 boolean 扩展为 'NONE' | boolean,并新增测试覆盖首次挂载时的 'NONE' 情况。

Changes

Cohort / File(s) Summary
样式就绪状态管理
src/hooks/useStatus.ts
styleReady 返回类型从 boolean 改为 `'NONE'
渲染守卫逻辑
src/CSSMotion.tsx
在 render memo 中添加守卫:若 styleReady === 'NONE' 则返回 null,阻止在样式未就绪时渲染子内容。
初始化状态测试
tests/CSSMotion.spec.tsx
新增测试,断言首次渲染(STATUS_NONE)时样式就绪信号为 'NONE' 并验证相应类名输出。

Estimated code review effort

🎯 3 (中等复杂度) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 初见春芽我轻跳,样式未就绪先藏眸
用『NONE』作旗帜,静待花开风自留
一旦备齐,舞步便连绵不休 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题准确反映了主要变更,即通过添加'NONE'状态和早期返回来跳过第一帧渲染,直到样式准备就绪,这正是PR的核心目的。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch render-steps

No actionable comments were generated in the recent review. 🎉


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tests/CSSMotion.spec.tsx`:
- Around line 495-510: The TypeScript error comes from using jest.fn(() => null)
without a parameter type so TypeScript infers the mock's call tuple as empty;
update the mock declaration for mockRender to include an explicit call signature
(e.g., use jest.fn<(arg: any) => any>(() => null) or jest.fn<[any], any>(() =>
null) or type it as jest.Mock<any, any>) so that mockRender.mock.calls[0][0] is
correctly typed; change the jest.fn instantiation where mockRender is declared
in the CSSMotion test.
🧹 Nitpick comments (2)
src/hooks/useStatus.ts (1)

314-321: 建议将 'NONE' 提取为命名常量,与现有的 STATUS_NONE 风格保持一致。

当前 'NONE' 作为魔法字符串散落在 useStatus.tsCSSMotion.tsx 两处,容易拼写错误且缺乏语义关联。建议在 interface.ts 中导出一个常量(如 STYLE_READY_NONE),在两处引用。

♻️ 建议的修改

src/interface.ts 中添加:

export const STYLE_READY_NONE = 'NONE' as const;

然后在 useStatus.tsCSSMotion.tsx 中使用该常量替代字符串字面量 'NONE'

src/CSSMotion.tsx (1)

187-195: 注意:'NONE' 是 truthy 值,会触发 idRef.current 自增。

Line 187 的 if (styleReady)styleReady === 'NONE' 时为 true(字符串是 truthy),这会使 idRef.current 递增并触发 useMemo 重新计算。当前逻辑下这是正确的行为——memo 重新执行后在 Line 193 返回 null,从而跳过首帧渲染。

但如果未来有人将 'NONE' 改为 falsy 值(如 0null),此处行为会静默改变。建议在 if 条件中显式表达意图:

-    if (styleReady) {
+    if (styleReady === true || styleReady === 'NONE') {

这样可以更清晰地传达"只有 false 时不更新"的语义。

Comment on lines 495 to 510
it('styleReady returns NONE on first mount when status is STATUS_NONE', () => {
const mockRender = jest.fn(() => null);

render(
<CSSMotion visible motionAppear motionName="test">
{mockRender}
</CSSMotion>,
);

// First render (prepare stage)
expect(mockRender.mock.calls[0][0]).toEqual(
expect.objectContaining({
className: 'test-appear test-appear-prepare test',
}),
);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

CI 构建失败:TypeScript 类型错误 TS2493,需要为 mock 函数添加参数类型。

流水线报错 Tuple type '[]' of length '0' has no element at index '0',原因是 jest.fn(() => null) 没有声明参数,TypeScript 推断 mock.calls 中每次调用的参数元组为 [],导致 calls[0][0] 无法通过类型检查。

🔧 修复建议
- const mockRender = jest.fn(() => null);
+ const mockRender = jest.fn((_props: Record<string, any>, _ref?: any) => null);

或者使用类型断言:

- expect(mockRender.mock.calls[0][0]).toEqual(
+ expect((mockRender.mock.calls[0] as [any, any])[0]).toEqual(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('styleReady returns NONE on first mount when status is STATUS_NONE', () => {
const mockRender = jest.fn(() => null);
render(
<CSSMotion visible motionAppear motionName="test">
{mockRender}
</CSSMotion>,
);
// First render (prepare stage)
expect(mockRender.mock.calls[0][0]).toEqual(
expect.objectContaining({
className: 'test-appear test-appear-prepare test',
}),
);
});
it('styleReady returns NONE on first mount when status is STATUS_NONE', () => {
const mockRender = jest.fn((_props: Record<string, any>, _ref?: any) => null);
render(
<CSSMotion visible motionAppear motionName="test">
{mockRender}
</CSSMotion>,
);
// First render (prepare stage)
expect(mockRender.mock.calls[0][0]).toEqual(
expect.objectContaining({
className: 'test-appear test-appear-prepare test',
}),
);
});
🧰 Tools
🪛 GitHub Actions: ✅ test

[error] 505-505: Command 'bunx tsc --noEmit' failed with TS2493: Tuple type '[]' of length '0' has no element at index '0'.

🤖 Prompt for AI Agents
In `@tests/CSSMotion.spec.tsx` around lines 495 - 510, The TypeScript error comes
from using jest.fn(() => null) without a parameter type so TypeScript infers the
mock's call tuple as empty; update the mock declaration for mockRender to
include an explicit call signature (e.g., use jest.fn<(arg: any) => any>(() =>
null) or jest.fn<[any], any>(() => null) or type it as jest.Mock<any, any>) so
that mockRender.mock.calls[0][0] is correctly typed; change the jest.fn
instantiation where mockRender is declared in the CSSMotion test.

@codecov
Copy link

codecov bot commented Feb 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.55%. Comparing base (634cdf2) to head (22d1d67).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master      #73      +/-   ##
==========================================
+ Coverage   98.52%   98.55%   +0.02%     
==========================================
  Files          11       11              
  Lines         408      414       +6     
  Branches      112      117       +5     
==========================================
+ Hits          402      408       +6     
  Misses          6        6              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@zombieJ zombieJ merged commit 8eb87ba into master Feb 11, 2026
10 checks passed
@zombieJ zombieJ deleted the render-steps branch February 11, 2026 07:05
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

此 PR 旨在修复使用 motionAppear 时会多渲染一帧的问题。解决方案是为 styleReady 引入一个新的状态 'NONE' 来跳过初始渲染。

但目前的实现存在一个严重问题:跳过渲染的条件过于宽泛。如果组件在挂载时可见(visible={true})但未启用 motionAppear,这会导致组件无法渲染。我提供了一个建议,使该条件更加具体。

此外,为了提高代码清晰度和可维护性,我建议改进测试用例的名称和注释。

Comment on lines +315 to +321
!mountedRef.current && currentStatus === STATUS_NONE
? // Appear
'NONE'
: // Enter or Leave
step === STEP_START || step === STEP_ACTIVE
? styleStep === step
: true,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

styleReady 设置为 'NONE' 的条件 !mountedRef.current && currentStatus === STATUS_NONE 过于宽泛。如果组件在挂载时 visible={true}motionAppear={false},这会导致组件渲染为 null 并且卡在这个状态。

该条件应该更具体,仅适用于 "appear" 动画。它应该检查 motionAppearvisible 属性,类似于为 STATUS_APPEAR 确定 nextStatus 的方式。

    !mountedRef.current && visible && motionAppear
      ? 'NONE'
      : step === STEP_START || step === STEP_ACTIVE
      ? styleStep === step
      : true,

Comment on lines +495 to +504
it('styleReady returns NONE on first mount when status is STATUS_NONE', () => {
const mockRender = jest.fn(() => null) as jest.Mock;
(mockRender as any).mock.calls = [] as any;

render(
<CSSMotion visible motionAppear motionName="test">
{mockRender}
</CSSMotion>,
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这个测试的标题和注释有点误导性。该测试并未直接检查 styleReady 的值,而是检查其导致跳过第一次渲染的后果。一个更具描述性的标题和注释将提高代码的清晰度和可维护性。

例如:

标题: it('should skip initial render frame when motionAppear is enabled')

注释: // The first render of children should be in the 'prepare' stage

Suggested change
it('styleReady returns NONE on first mount when status is STATUS_NONE', () => {
const mockRender = jest.fn(() => null) as jest.Mock;
(mockRender as any).mock.calls = [] as any;
render(
<CSSMotion visible motionAppear motionName="test">
{mockRender}
</CSSMotion>,
);
it('should skip initial render frame when motionAppear is enabled', () => {
const mockRender = jest.fn(() => null);
render(
<CSSMotion visible motionAppear motionName="test">
{mockRender}
</CSSMotion>,
);
// The first render of children should be in the 'prepare' stage

zombieJ added a commit that referenced this pull request Feb 11, 2026
This addresses the PR comment suggesting that styleReady should only
return 'NONE' when motionAppear is true, otherwise it should always
render the children.

Ref: #73 (comment)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant