@samitouri / QOS-React-1 / commits / 129154cedf

[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 {