fix: don't double-invoke effects for moved children in StrictMode#625
fix: don't double-invoke effects for moved children in StrictMode#625everettbu wants to merge 2 commits into
Conversation
Greptile SummaryThis PR fixes a StrictMode bug where reordering keyed children in an array incorrectly double-invoked effects (even with Additionally:
Confidence Score: 5/5
Important Files Changed
Last reviewed commit: a2b2040 |
| `[${nextDeps.map(String).join(', ')}]`, | ||
| `[${prevDeps.map(String).join(', ')}]`, |
There was a problem hiding this comment.
Pre-existing swapped labels for Previous/Incoming
The nextDeps and prevDeps arguments are passed in the wrong order relative to their labels. "Previous" is being populated with nextDeps and "Incoming" with prevDeps, which is backwards. Compare with ReactFiberHooks.js (lines 486-487), where the ordering is correct:
`[${prevDeps.map(String).join(', ')}]`, // ← "Previous"
`[${nextDeps.map(String).join(', ')}]`, // ← "Incoming"This bug predates this PR (existed before the .map(String) fix was applied), but since you're already touching these lines it would be good to fix the ordering:
| `[${nextDeps.map(String).join(', ')}]`, | |
| `[${prevDeps.map(String).join(', ')}]`, | |
| `[${prevDeps.map(String).join(', ')}]`, | |
| `[${nextDeps.map(String).join(', ')}]`, |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/react-server/src/ReactFizzHooks.js
Line: 154-155
Comment:
**Pre-existing swapped labels for Previous/Incoming**
The `nextDeps` and `prevDeps` arguments are passed in the wrong order relative to their labels. "Previous" is being populated with `nextDeps` and "Incoming" with `prevDeps`, which is backwards. Compare with `ReactFiberHooks.js` (lines 486-487), where the ordering is correct:
```js
`[${prevDeps.map(String).join(', ')}]`, // ← "Previous"
`[${nextDeps.map(String).join(', ')}]`, // ← "Incoming"
```
This bug predates this PR (existed before the `.map(String)` fix was applied), but since you're already touching these lines it would be good to fix the ordering:
```suggestion
`[${prevDeps.map(String).join(', ')}]`,
`[${nextDeps.map(String).join(', ')}]`,
```
How can I resolve this? If you propose a fix, please make it concise.When a hook's dependency array changes size and contains Symbol values, the error message generation crashes with "Cannot convert a Symbol value to a string" because Array.join() implicitly calls toString() on each element. Use String() explicitly to safely convert any dep value. Fixes #19848
In StrictMode, `doubleInvokeEffectsInDEVIfNecessary` fires for any fiber that has the `PlacementDEV` flag set. Previously, `placeChild` set `PlacementDEV` on both newly inserted fibers AND fibers that were moved to a different position in an array. This caused a regression in React 19: when a keyed child is reordered within an array, its effects are re-run in dev/StrictMode even when dependencies are empty `[]`. The same component in production, or in React 18, correctly skips effect re-runs for moves. The fix is to only set `PlacementDEV` for actual insertions (where `current === null`). Moved fibers (`current !== null`, `oldIndex < lastPlacedIndex`) still receive the `Placement` flag so the host node is correctly repositioned in the DOM, but StrictMode no longer treats them as new mounts. Also fix unused `ref3` variable in ReactFabric-test.internal.js introduced in #35912 (copy-paste: third View used ref2 instead of ref3). Fixes #32561
9a86561 to
a2b2040
Compare
|
Upstream PR was closed or merged. Code is synced via branch mirror. |
Mirror of facebook/react#35916
Original author: emmaeng700
Summary
Fixes #32561 — in React 19,
StrictModewas incorrectly re-running effects (even with[]dependencies) when a keyed child is reordered within an array. This does not happen in React 18 or in production builds.Root Cause
placeChild()inReactChildFiber.jssetsPlacementDEVon both newly inserted fibers and moved fibers (those with an existingalternatewhoseoldIndex < lastPlacedIndex). The StrictMode double-invoke logic indoubleInvokeEffectsInDEVIfNecessarytreats any fiber withPlacementDEVas a new mount and re-runs its effects.A moved fiber is not a new mount — it already has state, refs, and a lifecycle. Re-running its effects in dev mode is incorrect and diverges from production behavior.
Fix
Only set
PlacementDEVfor actual insertions (current === null). Moved fibers still receive thePlacementflag so the host node is repositioned in the DOM, butPlacementDEVis omitted so StrictMode does not treat them as new mounts.Test
Added a test in
StrictEffectsMode-test.jsthat renders[A, B, C]in StrictMode, reorders to[C, A, B], and asserts that no effect callbacks fire (no mount/unmount ofuseEffectoruseLayoutEffectwith[]deps).Checklist
placeChild()PlacementDEVpath is dev-only)