Skip to content

fix: don't double-invoke effects for moved children in StrictMode#35916

Open
emmaeng700 wants to merge 2 commits intofacebook:mainfrom
emmaeng700:fix/strict-mode-move-double-invoke
Open

fix: don't double-invoke effects for moved children in StrictMode#35916
emmaeng700 wants to merge 2 commits intofacebook:mainfrom
emmaeng700:fix/strict-mode-move-double-invoke

Conversation

@emmaeng700
Copy link

@emmaeng700 emmaeng700 commented Feb 26, 2026

Summary

Fixes #32561 — in React 19, StrictMode was 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() in ReactChildFiber.js sets PlacementDEV on both newly inserted fibers and moved fibers (those with an existing alternate whose oldIndex < lastPlacedIndex). The StrictMode double-invoke logic in doubleInvokeEffectsInDEVIfNecessary treats any fiber with PlacementDEV as 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 PlacementDEV for actual insertions (current === null). Moved fibers still receive the Placement flag so the host node is repositioned in the DOM, but PlacementDEV is omitted so StrictMode does not treat them as new mounts.

// Before
if (oldIndex < lastPlacedIndex) {
  // This is a move.
  newFiber.flags |= Placement | PlacementDEV; // <- wrong
}

// After
if (oldIndex < lastPlacedIndex) {
  // This is a move — DOM node moves but component is not newly mounting.
  newFiber.flags |= Placement; // PlacementDEV intentionally omitted
}

Test

Added a test in StrictEffectsMode-test.js that renders [A, B, C] in StrictMode, reorders to [C, A, B], and asserts that no effect callbacks fire (no mount/unmount of useEffect or useLayoutEffect with [] deps).

Checklist

  • Reproduces the bug with a new test
  • Fix is minimal and targeted — one line change in placeChild()
  • No behavior change in production (the PlacementDEV path is dev-only)
  • No behavior change for actual new insertions

@meta-cla meta-cla bot added the CLA Signed label Feb 26, 2026
@emmaeng700 emmaeng700 force-pushed the fix/strict-mode-move-double-invoke branch from 6b9aeac to 9a86561 Compare February 26, 2026 18:22
@react-sizebot
Copy link

Comparing: 98ce535...9a86561

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.84 kB 6.84 kB = 1.88 kB 1.88 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 611.76 kB 611.79 kB +0.01% 108.10 kB 108.11 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.84 kB 6.84 kB = 1.88 kB 1.88 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 677.69 kB 677.72 kB +0.01% 119.07 kB 119.08 kB
facebook-www/ReactDOM-prod.classic.js +0.08% 697.89 kB 698.44 kB +0.11% 122.63 kB 122.76 kB
facebook-www/ReactDOM-prod.modern.js +0.08% 688.21 kB 688.76 kB +0.11% 121.01 kB 121.15 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against 9a86561

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 facebook#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 facebook#35912 (copy-paste: third View used ref2 instead of ref3).

Fixes facebook#32561
@emmaeng700 emmaeng700 force-pushed the fix/strict-mode-move-double-invoke branch from 9a86561 to a2b2040 Compare February 26, 2026 19:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: StrictMode reruns effects when a child is moved in an array

2 participants