chore(test): run hook tests under React.StrictMode#2431
Merged
Conversation
Flip `configure({ reactStrictMode: true })` globally in
`packages/core/test/setup.ts` so every `render` and `renderHook` from
`vitest-browser-react` is automatically wrapped in `<React.StrictMode>`.
If a test passes under StrictMode it passes without; the inverse hides
real bugs (see #1991).
Simplifies `useSprings.test.tsx`: the local `isStrictMode` flag and
`<React.StrictMode>` wrapper in `createUpdater` are now redundant, so
both are dropped. The `strictModeFunctionCallMultiplier` is preserved
(still 2) because props functions are still invoked twice per render.
Refs #1991.
The `reverse` and `passedRef` flags were accumulated as side effects inside the `useSprings` wrapper, expecting it to fire at least once per render. Under React.StrictMode's second render pass `useSprings` skips the wrapper — its `useMemo([length])` deps are unchanged and the `[deps]` `useMemo` calls `declareUpdates(0, min(prevLength, length))` which is `(0, 0)` on a first render — so the accumulators stay at their initial values. `reverse` defaults to `true`, which inverts the chaining direction in the layout effect. For object-form props every spring receives the same props, so derive `reverse` and `passedRef` directly from `propsArg` instead of mutating them via the wrapper. The function-form path still uses the accumulator pattern; same latent issue, tracked as follow-up. Fixes #1991.
…sed springs `SpringValue.stop()` unconditionally called `_focus(this.get())` to snap `animation.to` to the current value. That is the right move when freezing an active animation, but it incorrectly establishes a goal where none existed on a paused or never-started spring (`_prepareNode` seeds the underlying value via `from`, so `this.get()` returns that value). The bug became observable under React.StrictMode, whose simulated unmount fires `useSprings`'s cleanup (`ctrl.stop(true)`) on every controller — including a freshly-mounted spring whose initial update was paused by `SpringContext`. The cleanup wrote `from` into `animation.to`, leaving `t.goal` equal to `0` instead of `undefined` on the subsequent remount. Gate the `_focus` call on `animation.to` already being defined. The "only merges when changed" test gets an updated first assertion that acknowledges the StrictMode mount → simulated unmount → remount cycle double-fires the layout effect: the user update fires twice, and the default-context update fires on the remount because `defaultProps` now has `onProps` set. Fixes #1991.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 8a43db7 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 |
This was referenced May 21, 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.
Run every hook test under
React.StrictModeby default. The premise: if a test passes under StrictMode it passes without, and the inverse hides real React-correctness bugs. The two fixes in this PR are exactly the kind of bug the inverse was hiding — both fall out the moment StrictMode is on.Bugs StrictMode caught
useTrailchained in the wrong direction.reverseandpassedRefwere accumulated as side effects inside theuseSpringswrapper, relying on the wrapper firing at least once per render. StrictMode's second render pass skips it —useMemo([length])deps are unchanged so the cache holds, and the[deps]useMemoinvokesdeclareUpdates(0, min(prevLength=0, length=2))which iterates zero times on the first render.reversestayed at its initialtrue, flipping the trail direction. Fixed by derivingreverse/passedReffrompropsArgdirectly for the object form. The function form has the same latent issue and is noted as follow-up.SpringValue.stop()established a phantom goal.stop()unconditionally called_focus(this.get())to snapanimation.toto the current value — correct for an active animation, wrong for a paused or never-started spring whose underlying value was seeded by_prepareNodeviafrom. StrictMode's simulated unmount firesuseSprings's cleanup (ctrl.stop(true)) on every controller, so a freshly-mounted paused spring ended up withanimation.to = 0instead ofundefined. Fixed by gating the_focuscall onanimation.toalready being defined.Stale assertion
SpringContext > only merges when changedneeded an updated first assertion — not a library bug, an unavoidable consequence of StrictMode double-firing effects on initial mount. The user update fires twice, and the default-context update fires on the remount becausedefaultProps.onPropswas set by the first pass. Subsequent rerenders are unaffected.