Skip to content

Fix AnimatePresence keeping exiting children during rapid updates with dynamic variants#3542

Merged
mattgperry merged 1 commit intomainfrom
animate-presence-rapid-switching
Feb 6, 2026
Merged

Fix AnimatePresence keeping exiting children during rapid updates with dynamic variants#3542
mattgperry merged 1 commit intomainfrom
animate-presence-rapid-switching

Conversation

@mattgperry
Copy link
Collaborator

Summary

  • Fixes exiting children getting stuck in the DOM when rapidly switching AnimatePresence children with dynamic custom variants (where animated properties differ between items)
  • Root cause: animateChanges() re-resolved exit variants on every re-render with the latest custom value, starting new value animations that interrupted the originals. Since JSAnimation.stop() doesn't resolve its finished promise, the exit completion chain hung forever.
  • Fix: skip re-processing the exit animation type in animateChanges() when it's already active and wasn't just explicitly toggled, preserving the running exit animation's promise chain

Fixes #3541

Test plan

  • Added test: exiting children are removed from DOM during rapid key switches with dynamic custom variants
  • Added test: onExitComplete fires reliably during rapid key switches with dynamic custom variants
  • All 42 AnimatePresence tests pass
  • All 25 animation-state tests pass
  • Full framer-motion test suite passes (739 tests, 0 failures)

🤖 Generated with Claude Code

…tes with dynamic variants

When rapidly switching children inside AnimatePresence with dynamic custom
variants (where animated properties differ between items), exiting children
could get stuck in the DOM. This happened because animateChanges() re-resolved
exit variants on every re-render with the latest custom value, starting new
value animations that interrupted the originals. Since JSAnimation.stop()
doesn't resolve its finished promise, the exit completion chain would hang
forever, preventing onExitComplete from firing and leaving elements in the DOM.

Fix: skip re-processing the exit animation type in animateChanges() when it's
already active and wasn't just explicitly toggled, preserving the running exit
animation's promise chain.

Fixes #3541

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude
Copy link

claude bot commented Feb 6, 2026

Code Review

Summary

This PR addresses a critical bug where exiting children get stuck in the DOM during rapid AnimatePresence updates with dynamic custom variants. The fix is well-designed, minimal, and correctly targets the root cause.


Code Quality ✅

Strengths:

  • Surgical precision: The fix adds only 17 lines to animation-state.ts:200-214, preserving the existing control flow
  • Proper guard placement: The check is positioned correctly after the basic skip conditions but before variant processing
  • Encounted keys preservation: The fix correctly merges prevResolvedValues into encounteredKeys to maintain the protected keys directory for lower-priority animation types
  • Clear documentation: The comment explains the bug mechanism concisely (re-resolving exit with changed custom stops running animations)
  • Follows codebase patterns: Uses optional chaining and early continue, consistent with CLAUDE.md style guidelines

Implementation details:

The condition is precise:

  • type === "exit" - Only applies to exit animations
  • typeState.isActive - Exit is currently running
  • activeDelta !== true - Exit wasn't just explicitly activated this render (would be true from setActive())

This prevents re-processing while allowing intentional re-triggering.


Test Coverage ✅

Excellent test design:

  1. Test 1 (lines 1248-1317): Verifies DOM cleanup

    • Rapidly switches through 4 items with alternating fade/slide transitions
    • Asserts only 1 child remains after animations complete
    • Directly reproduces the reported bug scenario
  2. Test 2 (lines 1319-1390): Verifies callback firing

Strengths:

  • Uses realistic dynamic variants with different animated properties (opacity vs x)
  • Tests rapid state changes (active={1} → active={2} → active={3})
  • Proper async handling with act(), setTimeout(), and nextFrame()
  • Follows CLAUDE.md guidance: "Always write a failing test FIRST"

Minor suggestion:
The tests use toBeGreaterThan(0) for exit complete count. Consider asserting the exact count (3 exits) for stricter verification.


Potential Issues ⚠️

Edge case consideration:
What happens if prevResolvedValues is empty/undefined during a running exit? The code handles this safely with the optional check, but it means encounteredKeys won't be updated. This appears correct - if there are no prev values, there's nothing to protect.

Question for follow-up:
Does this fix handle the case where:

  1. Exit animation starts with custom="fade"
  2. Parent's custom changes to "slide" mid-exit
  3. Exit should complete with original "fade" behavior

Based on the code, yes - the continue prevents re-resolving the variant, so it preserves the original animation. ✅


Performance Considerations ✅

Impact: Minimal

  • Adds one extra condition check per animation type per render
  • Object spread for encounteredKeys only when skipping exit (infrequent)
  • No additional loops or async operations
  • The continue actually improves performance by skipping unnecessary variant resolution

File size: +17 lines is negligible for a shipped library, especially given the bug severity.


Security Concerns ✅

No security implications. This is pure animation control flow logic.


Architecture & Best Practices ✅

Alignment with codebase:

  • Follows the variant priority system documented at animation-state.ts:100-108
  • Respects the encounteredKeys protection mechanism
  • Consistent with other early-exit patterns in animateChanges()

Root cause analysis:
The PR description clearly explains the issue - JSAnimation.stop() doesn't resolve its finished promise, so the exit completion chain hung forever.

This is the correct layer to fix it - preventing interruption rather than fixing promise resolution in JSAnimation, which could have broader implications.


Recommendations

  1. ✅ Approve and merge - This is a solid bug fix
  2. Consider adding a comment linking to issue [BUG] AnimatePresence keeps exiting children in DOM during rapid updates with dynamic variants #3541 in the code for future reference
  3. The test assertion could be stricter (exact count vs > 0)
  4. Verify the full test suite passes (PR says 739 tests pass ✅)

Final Verdict: LGTM 🚀

This is a well-researched, minimally invasive fix with excellent test coverage. The implementation correctly preserves running exit animations while maintaining the protected keys system. Great work!

Tested scenarios:

  • ✅ Rapid key switches with dynamic variants
  • ✅ DOM cleanup
  • ✅ Callback firing
  • ✅ All existing tests pass (42 AnimatePresence + 25 animation-state tests)

@mattgperry mattgperry merged commit 3a99c56 into main Feb 6, 2026
6 checks passed
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.

[BUG] AnimatePresence keeps exiting children in DOM during rapid updates with dynamic variants

1 participant