-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix AnimatePresence exit animations with Radix UI asChild #3457
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
PR ReviewSummaryThis PR successfully fixes a critical regression where AnimatePresence exit animations were not running when using Radix UI's ✅ Code Quality & Best PracticesStrengths:
Style compliance:
✅ Implementation CorrectnessThe core change is sound:
Key correctness points:
✅ Test CoverageExcellent test coverage:
Test quality:
🔍 Potential ConsiderationsMinor observations (not blocking):
✅ PerformanceNo performance concerns:
✅ SecurityNo security concerns identified. The changes are internal to the ref management system and don't introduce any attack vectors. 🎯 Final AssessmentRecommendation: APPROVED ✅ This is a well-crafted fix that:
The implementation is clean, the testing is thorough, and the PR description clearly explains the problem, root cause, and solution. This is exactly how bug fixes should be done. Excellent work! 🚀 |
9ffce23 to
9ac5155
Compare
Code ReviewI've reviewed this PR and the implementation looks solid overall. Here's my detailed feedback: ✅ Strengths
🔍 Observations & Potential Concerns1. useInsertionEffect Dependency ArrayuseInsertionEffect(() => {
externalRefContainer.current = externalRef
})Issue: This Analysis: While this works correctly (the ref is updated before layout effects), it's unconventional. Looking at other
Recommendation: Consider adding useInsertionEffect(() => {
externalRefContainer.current = externalRef
}, [externalRef])This would:
2. Edge Case: Initial RenderThe
This is correct, but in the unlikely event that 3. Type SafetyThe code correctly handles all ref types (callback refs, ref objects, and React 19 cleanup functions). No issues here. 4. PerformanceThe fix actually improves performance by reducing unnecessary ref callback invocations. Previously, Radix's changing callback refs would trigger the whole ref callback to be recreated and called with null then the new instance. Now it's stable. ✅ 🔒 Security ConsiderationsNo security concerns. The changes are limited to ref handling and don't introduce any injection vulnerabilities, XSS risks, or unsafe operations. 🐛 Potential BugsNone identified. The logic correctly:
📊 Test Coverage AssessmentExcellent coverage:
Optional enhancement: Consider adding a unit test that directly tests 📝 Code Quality
🎯 VerdictApprove with minor suggestion: The PR successfully fixes the AnimatePresence bug with Radix UI's Suggested change (non-blocking): Add Great work tracking down this subtle bug! The fix is elegant and the test coverage ensures it won't regress. 🎉 |
When using Radix UI's asChild prop with motion components inside AnimatePresence, exit animations were not running. This was because externalRef was in the useMotionRef useCallback dependency array, causing the callback to be recreated whenever Radix created a new composed ref on render. The fix stores externalRef in a ref to access the current value without including it in the dependency array. This preserves: - React 19 cleanup function support - Proper ref forwarding - AnimatePresence exit animations with libraries like Radix UI Added E2E test with @radix-ui/react-dialog to verify the fix. Fixes #3455 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
9ac5155 to
d6772da
Compare
Code Review: Fix AnimatePresence exit animations with Radix UI asChildSummaryThis PR successfully addresses a critical bug where AnimatePresence exit animations were not running when using Radix UI's ✅ Strengths1. Root Cause AnalysisThe PR correctly identifies the issue: Radix UI creates new callback refs on each render with 2. Clean Solution (use-motion-ref.ts:24-27)Using
3. Comprehensive Test Coverage
4. Code Style Compliance
💡 Observations & Suggestions1. React 19 Cleanup Function SupportThe new implementation at line 40-44 correctly passes through callback ref return values (React 19 cleanup functions), but this behavior is no longer explicitly tested since the tests were simplified. Consideration: The E2E test at 2. Potential Edge Case: Ref SwitchingThe test at line 50-64 in const externalRefContainer = useRef(externalRef)
useInsertionEffect(() => {
externalRefContainer.current = externalRef
})On the first render,
3. Type Assertion (line 43);(ref as React.MutableRefObject<Instance>).current = instanceThis type assertion is safe because we check } else if (ref) {
(ref as React.MutableRefObject<Instance>).current = instance
}The leading semicolon is defensive programming for ASI but unnecessary here since the previous line doesn't create ambiguity. 4. Dependency Array (line 46)[visualElement]Good! Removing
🔒 Security ConsiderationsNo security concerns identified. The changes:
⚡ Performance ConsiderationsPositive impact:
No performance regressions expected. 🧪 Test CoverageExcellent coverage:
Minor gap: React 19 cleanup function behavior tested in E2E but not verified in the Cypress test. 📝 Code QualityOverall: Excellent
🎯 RecommendationAPPROVE ✅ This is a high-quality fix that:
Optional Follow-ups (non-blocking):
Great work solving this tricky interaction between Framer Motion, React refs, and Radix UI! 🚀 |
Summary
asChildprop with motion componentsexternalRefin a ref instead of including it inuseCallbackdependency arrayProblem
When using Radix UI Dialog (or similar libraries) with
asChildand motion components insideAnimatePresence, exit animations were not running. The elements were immediately removed from the DOM instead of animating out.Root Cause
In
use-motion-ref.ts,externalRefwas in theuseCallbackdependency array. Radix UI'sasChildcreates a new composed callback ref on each render. This caused:useCallbackto return a new function each rendernull(unmount) then the new instance (remount)Solution
Store
externalRefin a ref (externalRefRef) and access the current value inside the callback, rather than including it in dependencies. This maintains:Test plan
@radix-ui/react-dialogreproducing the exact issue from [BUG] - Possible AnimatePresence regression in 12.24.4 #3455Fixes #3455
🤖 Generated with Claude Code