fix(core): stop superseded loop chains on re-render#2471
Merged
Conversation
A re-render that flipped `loop` from truthy to falsy did not stop the animation, because the in-flight `flushUpdate` chain captured its own `loop` value and kept scheduling iterations alongside the new update. The Controller now tags each loop chain with a generation token and exits the recursion when a newer external update has superseded it. Closes #1193.
🦋 Changeset detectedLatest commit: 1e04dc1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This was referenced May 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1193.
Summary
Toggling
loopfrom truthy to falsy on a re-render did not actually stop the animation. The in-flightflushUpdatechain captured its ownloopvalue in a closure, so the capturedtruekept the recursion alive even when the new top-level update arrived withloop: false. Both chains then ran in parallel — the old one scheduled iterations indefinitely, the new one was a no-op.Fix
Controllernow carries a_lastLoopIdgeneration token. A top-level external update bumps the token only when it could express a different loop intent (i.e. it mentionsloopand is not an internal sub-step fromrunAsync'sanimate, which carriesparentId, nor apause/resumelifecycle call). Each loop chain captures its token; before recursing into the next iteration it checks the token still matches_lastLoopIdand otherwise exits.This is the minimal scoping required to avoid regressions in the three existing loop tests that rely on
runAsyncsub-steps and pause/resume cycles preserving the chain.A regression test mirroring the reported shape (
loop: true → loop: falsevia a secondctrl.start) is added inController.test.ts.