Skip to content

fix: events not firing when SpringRef attached manually under StrictMode#2430

Merged
joshuaellis merged 1 commit into
nextfrom
fix/1991
May 21, 2026
Merged

fix: events not firing when SpringRef attached manually under StrictMode#2430
joshuaellis merged 1 commit into
nextfrom
fix/1991

Conversation

@joshuaellis
Copy link
Copy Markdown
Member

Closes #1991.

Summary

When a user attaches a SpringRef manually via the ref prop on useSpring / useSprings, events (onStart, onChange, onRest) silently stop firing under React StrictMode, even though the animation itself still plays.

Root cause

flushUpdate in Controller.ts mutates the input props in place — it wraps each event handler (onStart, onChange, onRest) with a batching closure so multiple per-frame invocations collapse into one. The wrapper captures the user's original handler in its closure.

useSprings' commit-phase layout effect pushes updates.current[i] (a stable reference reused across renders) onto ctrl.queue whenever a ref is attached. Under StrictMode the layout effect runs twice (mount → cleanup → re-mount), so the already-wrapped handler from the first flush gets wrapped a second time. The outer wrapper now closes over wrapper1 instead of the user's callback, so when _onFrame flushes the event Map it only ever invokes wrapper1 — which just adds itself to the queue. The user's handler is never reached.

Fix

Push a shallow copy of the update (including its default sub-object) onto ctrl.queue, so flushUpdate's mutations land on the copy and never leak back into the canonical updates.current[i] reference.

One-line change in packages/core/src/hooks/useSprings.ts.

Closes #1991

flushUpdate mutates the input props in place by wrapping event handlers
(onStart/onChange/onRest) with a batching closure. useSprings' commit-phase
layout effect pushes updates.current[i] — a stable reference reused across
renders — onto ctrl.queue. Under StrictMode the layout effect runs twice,
so the already-wrapped handler from the first flush gets wrapped again.
The outer wrapper now closes over wrapper1 instead of the user's callback,
so when _onFrame flushes the event Map it only ever invokes wrapper1 —
which just adds itself to the queue. The user's handler is never reached.

Push a shallow copy of the update (including its default sub-object) onto
ctrl.queue so flushUpdate's mutations land on the copy and never leak back
into the canonical updates.current[i] reference.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 21, 2026

🦋 Changeset detected

Latest commit: 4c64204

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 4:08pm

Request Review

@joshuaellis joshuaellis merged commit 8da5e50 into next May 21, 2026
15 checks passed
@joshuaellis joshuaellis deleted the fix/1991 branch May 21, 2026 16:11
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.

[bug]: Events aren't called when attaching a ref manually

1 participant