[Fiber] Don't invoke effects on moved children in StrictMode (#36948)
Fixes #29585.
Sophie Alpert committed
Jul 7, 2026 at 09:09 UTC
129154cedf159a4f4a76634889e98d06760278cc
2 files changed
+70
-2
packages/react-reconciler/src/ReactChildFiber.js
+4
-2
@@ -524,8 +524,10 @@ function createChildReconciler(
524
if (current !== null) {
525
const oldIndex = current.index;
526
if (oldIndex < lastPlacedIndex) {
527
- // This is a move.
528
- newFiber.flags |= Placement | PlacementDEV;
527
+ // This is a move. The fiber already existed, so this is not a new
528
+ // mount; don't set PlacementDEV, which would cause StrictMode to
529
+ // re-run the effects in its subtree as if it had remounted.
530
+ newFiber.flags |= Placement;
531
return lastPlacedIndex;
532
} else {
533
// This item can stay in place.
packages/react-reconciler/src/__tests__/StrictEffectsMode-test.js
+66
@@ -628,6 +628,72 @@ describe('StrictEffectsMode', () => {
628
}
629
});
630
631
+ it('reordering keyed children does not re-run effects', async () => {
632
+ const log = [];
633
+ function Child({label}) {
634
+ React.useEffect(() => {
635
+ log.push(`${label} useEffect mount`);
636
+ return () => log.push(`${label} useEffect unmount`);
637
+ }, []);
638
+ React.useLayoutEffect(() => {
639
+ log.push(`${label} useLayoutEffect mount`);
640
+ return () => log.push(`${label} useLayoutEffect unmount`);
641
+ }, []);
642
+
643
+ return null;
644
+ }
645
+
646
+ function App({keys}) {
647
+ return keys.map(key => <Child key={key} label={key} />);
648
+ }
649
+
650
+ await act(() => {
651
+ ReactNoop.render(
652
+ <React.StrictMode>
653
+ <App keys={['a', 'b']} />
654
+ </React.StrictMode>,
655
+ );
656
+ });
657
+
658
+ if (__DEV__) {
659
+ expect(log).toEqual([
660
+ 'a useLayoutEffect mount',
661
+ 'b useLayoutEffect mount',
662
+ 'a useEffect mount',
663
+ 'b useEffect mount',
664
+ 'a useLayoutEffect unmount',
665
+ 'b useLayoutEffect unmount',
666
+ 'a useEffect unmount',
667
+ 'b useEffect unmount',
668
+ 'a useLayoutEffect mount',
669
+ 'b useLayoutEffect mount',
670
+ 'a useEffect mount',
671
+ 'b useEffect mount',
672
+ ]);
673
+ } else {
674
+ expect(log).toEqual([
675
+ 'a useLayoutEffect mount',
676
+ 'b useLayoutEffect mount',
677
+ 'a useEffect mount',
678
+ 'b useEffect mount',
679
+ ]);
680
+ }
681
+
682
+ // Reordering existing children must not re-run their effects. The
683
+ // components are neither unmounted nor remounted, and their effect
684
+ // dependencies have not changed.
685
+ log.length = 0;
686
+ await act(() => {
687
+ ReactNoop.render(
688
+ <React.StrictMode>
689
+ <App keys={['b', 'a']} />
690
+ </React.StrictMode>,
691
+ );
692
+ });
693
+
694
+ expect(log).toEqual([]);
695
+ });
696
+
697
it('classes and functions are double invoked together correctly', async () => {
698
const log = [];
699
class ClassChild extends React.PureComponent {