Fix duplicate exit animation processing in AnimatePresence#3454
Fix duplicate exit animation processing in AnimatePresence#3454mattgperry merged 1 commit intomainfrom
Conversation
PR Review: Fix duplicate exit animation processing in AnimatePresenceSummaryThis PR addresses an important edge case where ✅ Strengths
🔍 Code Quality ObservationsPotential Race Condition (Minor)Location: The guard check happens before setting the const onExit = () => {
if (exitingComponents.current.has(key)) {
return // Early return - exitComplete never gets set to true
}
exitingComponents.current.add(key)
if (exitComplete.has(key)) {
exitComplete.set(key, true) // Only reached on first call
}Scenario: If Verdict: This is actually correct behavior. The guard prevents duplicate processing, and only the first call should update state. Not an issue. Missing Edge Case Test (Suggestion)The tests cover the happy path well, but consider adding a test for this scenario:
Expected behavior: The Set should be cleared when the component re-enters (line 117), allowing proper exit processing on the second exit. Suggestion: Add a test like: test("Component can exit again after re-entering", async () => {
// Child exits -> re-enters -> exits again
// Verify onExitComplete is called both times (once per exit cycle)
})This would verify the cleanup logic at line 117 works correctly. 🎯 Best Practices & Performance✅ Memory Management: Proper cleanup in the effect (line 117) prevents memory leaks 🔒 SecurityNo security concerns identified. This is internal state management with no user input or external data handling. 🧪 Test CoverageCurrent Coverage: Good
Suggested Addition:
Test Pattern Observation: Tests use 📝 Minor Suggestions
✨ Overall AssessmentVerdict: Approve with minor suggestions ✅ This is a solid fix that:
The implementation is production-ready. The suggestions above are minor enhancements that could be addressed now or in a future PR. Recommendation: Merge after considering the re-entry edge case test. If the existing test suite already covers component re-entry scenarios elsewhere, this may not be necessary. Nice work! This fix will prevent subtle bugs in applications using AnimatePresence with UI libraries that fire rapid events. |
Prevents AnimatePresence from processing exit animations multiple times when receiving rapid sequential events (e.g., Radix UI dismissable layers). Adds a Set to track components currently processing exit and guards against duplicate onExit calls. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
34e3a2b to
aa8b46b
Compare
Code ReviewI've reviewed PR #3454 and have the following feedback: ✅ Strengths1. Problem Identification & Solution
2. Test Coverage
3. Code Quality
🔍 Observations & Considerations1. Cleanup Logic ✅ exitingComponents.current.delete(key)This ensures that when a component re-enters the tree, it can exit again later. The third test validates this works correctly. 2. Concurrent React Safety
However, there's a subtle consideration: in React Concurrent Mode, multiple renders can be attempted and discarded. The current implementation should be safe because:
3. Edge Case - Component Re-mounting During Exit
The current code handles this correctly via line 117, which removes the key from the Set when the component re-enters. ✅ 4. Memory Leak Prevention
No memory leak concerns identified. 📊 Performance
🔒 SecurityNo security concerns identified. This is internal state management logic with no external inputs or potential for injection/XSS. 🧪 Test QualityThe tests are well-written but could benefit from one small improvement: Minor Suggestion: Consider using // Instead of nested setTimeout callbacks, could use:
async function wait(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}
// Then in test:
await wait(150)
expect(onExitCompleteCount).toBe(1)
// etc.But this is a very minor style preference - the current approach is consistent with existing tests in the file. 📝 Minor NitLine 188-191: Consider whether this early return needs a comment explaining why we silently return vs throwing/warning. The current behavior seems correct (fail silently to prevent duplicate processing), but a brief comment could help future maintainers: const onExit = () => {
// Prevent duplicate exit processing (can happen with rapid events)
if (exitingComponents.current.has(key)) {
return
}
exitingComponents.current.add(key)But this is very minor - the JSDoc on line 89-91 already explains the Set's purpose. ✅ Final VerdictThis is a well-crafted fix that:
Recommendation: Approve and merge The implementation is solid and the tests give confidence that this won't regress. The minimal, targeted change is exactly what you want for a bug fix. Nice work! This is a clean fix for a tricky race condition issue. 🎯 |
Summary
Setto track components currently processing exit and guards against duplicateonExitcallsusePresenceto verify the fix without relying on animation timingTest plan
onExitCompleteis called only once whensafeToRemoveis called multiple times🤖 Generated with Claude Code