Skip to content

fix: compose existing child refs in CSSMotion#76

Merged
zombieJ merged 8 commits intoreact-component:masterfrom
QDyanbing:image
Mar 25, 2026
Merged

fix: compose existing child refs in CSSMotion#76
zombieJ merged 8 commits intoreact-component:masterfrom
QDyanbing:image

Conversation

@QDyanbing
Copy link
Copy Markdown
Contributor

@QDyanbing QDyanbing commented Mar 25, 2026

背景

在 React 19 场景下,CSSMotion 的子节点如果本身已经持有 ref,当前实现不会再挂载内部的 motion ref。
这会导致 CSSMotion 在某些场景下拿不到真实 DOM,进而无法正确监听离场动画结束事件。

这个问题在 @rc-component/image / antd 的 Image Preview 关闭流程里会被稳定触发:
关闭后节点进入 leave 状态,但不会真正卸载,最终表现为透明遮罩残留、页面无法继续点击。

相关问题:

fix: ant-design/ant-design#57431

改动

  • CSSMotion 中合并子节点已有的 ref 和内部 nodeRef
  • 不再在“子节点已有 ref”时跳过内部 ref 注入
  • 新增回归测试,覆盖“子节点已有 ref 时,离场动画仍能正常结束并卸载”的场景

根因说明

当前逻辑在检测到 child 已有 ref 时,只保留 child 自己的 ref,导致 motion 内部拿不到 DOM 节点。
这样一来,transitionend / animationend 无法正确绑定到目标元素,离场动画结束后也就无法进入最终清理流程。

本次修复后,child 的原始 ref 语义保持不变,同时 motion 也能拿到同一个 DOM 节点。

影响评估

  • 不涉及公开 API 变更
  • 不修改 motion 状态机
  • 不改变 enter / leave 的时序逻辑
  • 仅修复“child 已有 ref 时 motion 内部 ref 丢失”的问题

Summary by CodeRabbit

发布说明

  • Bug修复

    • 改进组件对外部 ref 与子元素 ref 的合并与处理逻辑,提升过渡与渲染场景下的稳定性,避免错误引用导致的节点识别或行为异常。
    • 调整列表组件中 children 渲染函数的调用方式:仅在需要时传入第二个 ref 参数,确保单参渲染回调不意外接收 ref,从而避免引用冲突与渲染差异。
  • 测试

    • 新增严格模式下的过渡测试,验证合并后过渡结束与卸载流程正确执行,且不会触发不必要的 DOM 查找。

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 25, 2026

@QDyanbing is attempting to deploy a commit to the React Component Team on Vercel.

A member of the Team first needs to authorize it.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, 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!

此拉取请求修复了 CSSMotion 在 React 19 环境下,当子节点已持有 ref 时无法正确获取真实 DOM 的问题。此前,这会导致离场动画无法正常结束,进而造成组件无法卸载,例如透明遮罩残留。通过合并子节点的 refCSSMotion 内部的 nodeRef,确保了动画事件监听的正确性,从而解决了动画结束后组件无法清理的问题,同时不影响现有 API 和状态机。

Highlights

  • Ref 合并: 在 CSSMotion 中合并子节点已有的 ref 和内部 nodeRef,确保内部 motion ref 始终被挂载。
  • Ref 注入逻辑调整: 移除了当子节点已持有 ref 时跳过内部 ref 注入的逻辑。
  • 新增回归测试: 新增了回归测试,验证在子节点已有 ref 的情况下,离场动画仍能正常结束并卸载。

🧠 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.

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.

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
Copy Markdown

coderabbitai bot commented Mar 25, 2026

Warning

Rate limit exceeded

@zombieJ has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 18 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1521641c-f6a4-436c-a445-e554ae7c5984

📥 Commits

Reviewing files that changed from the base of the PR and between 9fb85c5 and 3e84e1b.

📒 Files selected for processing (2)
  • src/CSSMotion.tsx
  • src/CSSMotionList.tsx

Walkthrough

重构 genCSSMotion 的子元素 ref 注入与调用时机:仅在子元素可接收 ref 且子元素原始 ref 与内部 nodeRef 不同时,使用 composeRef(originNodeRef, nodeRef) 合并并克隆注入;将克隆/合并步骤移出 memo 化返回流程。新增严格模式测试覆盖行为。

Changes

Cohort / File(s) Summary
引用处理更新
src/CSSMotion.tsx
调整 genCSSMotion 的 ref 注入逻辑:先用 supportNodeRef 判断是否注入,读取 originNodeRef 并与内部 nodeRef 比较,仅在 originNodeRef !== nodeRef 时通过 composeRef(originNodeRef, nodeRef) 克隆并注入合成 ref;将注入逻辑移出 memo 的返回值处理。
调用约定调整(列表)
src/CSSMotionList.tsx
基于 children.length < 2 区分渲染回调调用方式:对单参 children 不再传入第二个 ref 参数,改变了传递 ref 的行为。
引用组合测试
tests/CSSMotion.spec.tsx
新增严格模式测试:验证当外部传入 ref 且子元素已有 ref 时,motion 的 ref 指向子元素 DOM;在隐藏后触发 transitionend 能移除元素;过程中未调用 ReactDOM.findDOMNode

Sequence Diagram(s)

(无)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 我在草丛轻跳,合并 ref 把线缝,
子元素安放妥当,变更悄无声从容,
严格模式里测试,DOM 与心都对齐,
啾啾鼓掌庆一刻,毛绒尾巴摆成弧。

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题清晰准确地描述了主要改变:修复 CSSMotion 中的子组件 refs 合并问题,与代码变更内容高度相关。
Linked Issues check ✅ Passed PR 变更直接解决了 #57431 中的问题:通过合并子组件的现有 ref 与内部 nodeRef,确保 CSSMotion 能够访问 DOM 节点以绑定过渡结束事件,从而完成离开动画并正确清理 DOM。
Out of Scope Changes check ✅ Passed 所有变更都在解决问题的范围内:修改 ref 注入逻辑、添加相关测试、调整 CSSMotionList 的 children 调用方式,均与修复 ref 丢失问题直接相关,无超出范围的改动。
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Copy Markdown

@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

This pull request enhances the CSSMotion component's ref handling by ensuring that its internal nodeRef is always applied, even when the child component already has its own ref. It achieves this by importing and utilizing composeRef to merge existing and new refs, preventing potential ref conflicts. A new test case has been added to tests/CSSMotion.spec.tsx to validate this improved ref composition behavior, particularly for motion end scenarios. There are no review comments to address.

Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
src/CSSMotion.tsx (1)

249-253: 注释与实现已不一致,建议同步更新。

当前实现已变为“始终注入 ref(必要时合并)”,但注释仍描述“仅在 child 无 ref 时注入”。建议改成准确描述,避免后续误判。

建议修改
-        // Auto inject ref if child node not have `ref` props
+        // Always inject motion ref; compose with child's existing ref when present
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/CSSMotion.tsx` around lines 249 - 253, The comment above the
ref-injection logic in CSSMotion.tsx is outdated: the code now always injects a
ref (merging when the child already has one) but the comment says it only
injects when the child has no ref; update the comment to reflect the current
behavior. Edit the comment near the block that checks
React.isValidElement(motionChildren) and supportRef(motionChildren) (inside the
CSSMotion component) to state that the component will always inject/merge a ref
into the child element when supported, mentioning that it merges with an
existing ref if present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/CSSMotion.tsx`:
- Around line 249-253: The comment above the ref-injection logic in
CSSMotion.tsx is outdated: the code now always injects a ref (merging when the
child already has one) but the comment says it only injects when the child has
no ref; update the comment to reflect the current behavior. Edit the comment
near the block that checks React.isValidElement(motionChildren) and
supportRef(motionChildren) (inside the CSSMotion component) to state that the
component will always inject/merge a ref into the child element when supported,
mentioning that it merges with an existing ref if present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9b66e2ce-f2c3-4313-89e5-47608fa4e37a

📥 Commits

Reviewing files that changed from the base of the PR and between 3ea1a5d and b9a0822.

📒 Files selected for processing (2)
  • src/CSSMotion.tsx
  • tests/CSSMotion.spec.tsx


const canHoldRef =
React.isValidElement(motionChildren) && supportRef(motionChildren);
const originNodeRef = canHoldRef ? getNodeRef(motionChildren) : null;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

我微调一下

Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/CSSMotionList.tsx`:
- Around line 177-179: The current logic in CSSMotionList.tsx that checks
children.length to decide whether to inject ref is unreliable and can drop refs
(see children usage and nodeRef propagation); change to use an explicit boolean
capability flag (e.g., shouldAutoInjectRef or a prop like forwardRef passed in
by the caller) instead of Function.length, update the rendering branch that
currently chooses between props => children({...}, undefined as any) and (props,
ref) => children({...}, ref) to read that explicit flag, and make the same
change in CSSMotion.tsx (the existing shouldAutoInjectRef logic around line 182)
so both components accept and use the explicit marker to decide ref injection
and ensure nodeRef is forwarded correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cecfb9c9-a01b-46cb-b0b1-ce22ee343990

📥 Commits

Reviewing files that changed from the base of the PR and between c1d0fe6 and 9fb85c5.

📒 Files selected for processing (2)
  • src/CSSMotion.tsx
  • src/CSSMotionList.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/CSSMotion.tsx

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 25, 2026

Codecov Report

❌ Patch coverage is 84.61538% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.09%. Comparing base (3ea1a5d) to head (3e84e1b).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/CSSMotionList.tsx 50.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #76      +/-   ##
==========================================
- Coverage   98.56%   98.09%   -0.47%     
==========================================
  Files          11       11              
  Lines         417      421       +4     
  Branches      120      119       -1     
==========================================
+ Hits          411      413       +2     
- Misses          6        7       +1     
- Partials        0        1       +1     

☔ 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 f6cc6fa into react-component:master Mar 25, 2026
5 of 8 checks passed
@QDyanbing QDyanbing deleted the image branch March 26, 2026 00:17
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.

Image: Preview not closing as expected

2 participants