Skip to content

fix(core): run async script to to completion under skipAnimation#2438

Merged
joshuaellis merged 1 commit into
nextfrom
worktree-1429
May 21, 2026
Merged

fix(core): run async script to to completion under skipAnimation#2438
joshuaellis merged 1 commit into
nextfrom
worktree-1429

Conversation

@joshuaellis
Copy link
Copy Markdown
Member

Closes #1429.

When Globals.skipAnimation is true, async function tos (e.g. enter: () => async next => await next({ opacity: 1 })) should land on the same end state as if animations had run normally — that's how the reduced-motion contract is documented in useReducedMotion. Today they don't: runAsync short-circuits before the script ever calls target.start, so values declared inside the script are silently dropped. Users have had to detect skipAnimation themselves and rewrite their async chains as flat immediate: true objects (workaround in the thread).

Behaviour now

runAsync.animate() applies each next(...) call with immediate: true and lets the script run through to completion. End states under skipAnimation:

to shape Lands on
{ x: 2 } x === 2 (unchanged)
[{ x: 1 }, { x: 2 }] x === 2
async next => { await next({ x: 1 }); await next({ x: 2 }) } x === 2
async next => { while (true) await next({ x: n++ }) } bails after 1024 next calls

The 1024-call cap

Without animation frames to pace it, while (true) await next(...) becomes a tight microtask loop that would hang the host. The cap is generous enough to be invisible for real chains and acts as a hard stop for runaway scripts. If you want repeating animations, use the loop prop — it does the right thing under skipAnimation.

Behavioural change to be aware of

Previously, setting skipAnimation mid-flight would abort the async script on its next await next(...). It now lets the script finish naturally — each remaining step jumps to its target instead of being skipped over. In-flight SpringValue animations are unchanged: they still ride out their remaining frames (the global flag is only checked when a new animation starts).

When `Globals.skipAnimation` was true, an async script `to` (e.g. `enter:
() => async next => await next({ opacity: 1 })`) bailed before applying any
values, so the spring never reached its declared end state. Apply each step
with `immediate: true` and let the script run to completion so the spring
lands on the final `next(...)` value just like it would with animations on.
A 1024-call safety cap protects against unterminating scripts that would
otherwise become a tight microtask loop without animation frames to pace them.

Closes #1429
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 21, 2026

🦋 Changeset detected

Latest commit: dd3933e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@react-spring/core Patch
@react-spring/animated Patch
@react-spring/mock-raf Patch
@react-spring/parallax Patch
@react-spring/rafz Patch
@react-spring/shared Patch
@react-spring/types Patch
@react-spring/konva Patch
@react-spring/native Patch
@react-spring/three Patch
@react-spring/web Patch
@react-spring/zdog Patch

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

@vercel
Copy link
Copy Markdown

vercel Bot commented May 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-spring Ready Ready Preview May 21, 2026 7:56pm

Request Review

@joshuaellis joshuaellis merged commit 01810a4 into next May 21, 2026
16 checks passed
@joshuaellis joshuaellis deleted the worktree-1429 branch May 21, 2026 19:59
This was referenced May 21, 2026
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.

skipAnimations breaks enter defined as function

1 participant