diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js index c356ed4f38e59..586fcda415b9e 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js @@ -3211,12 +3211,20 @@ function recursivelyTraverseAndDoubleInvokeEffectsInDEV( } let child = parentFiber.child; while (child !== null) { - doubleInvokeEffectsInDEV(root, child, isInStrictMode); + doubleInvokeEffectsInDEVIfNecessary(root, child, isInStrictMode); child = child.sibling; } } -function doubleInvokeEffectsInDEV( +// Unconditionally disconnects and connects passive and layout effects. +function doubleInvokeEffectsOnFiber(root: FiberRoot, fiber: Fiber) { + disappearLayoutEffects(fiber); + disconnectPassiveEffect(fiber); + reappearLayoutEffects(root, fiber.alternate, fiber, false); + reconnectPassiveEffects(root, fiber, NoLanes, null, false); +} + +function doubleInvokeEffectsInDEVIfNecessary( root: FiberRoot, fiber: Fiber, parentIsInStrictMode: boolean, @@ -3224,22 +3232,45 @@ function doubleInvokeEffectsInDEV( const isStrictModeFiber = fiber.type === REACT_STRICT_MODE_TYPE; const isInStrictMode = parentIsInStrictMode || isStrictModeFiber; - if (fiber.flags & PlacementDEV || fiber.tag === OffscreenComponent) { + // First case: the fiber **is not** of type OffscreenComponent. No + // special rules apply to double invoking effects. + if (fiber.tag !== OffscreenComponent) { + if (fiber.flags & PlacementDEV) { + setCurrentDebugFiberInDEV(fiber); + if (isInStrictMode) { + doubleInvokeEffectsOnFiber(root, fiber); + } + resetCurrentDebugFiberInDEV(); + } else { + recursivelyTraverseAndDoubleInvokeEffectsInDEV( + root, + fiber, + isInStrictMode, + ); + } + return; + } + + // Second case: the fiber **is** of type OffscreenComponent. + // This branch contains cases specific to Offscreen. + if (fiber.memoizedState === null) { + // Only consider Offscreen that is visible. + // TODO (Offscreen) Handle manual mode. setCurrentDebugFiberInDEV(fiber); - const isNotOffscreen = fiber.tag !== OffscreenComponent; - // Checks if Offscreen is being revealed. For all other components, evaluates to true. - const hasOffscreenBecomeVisible = - isNotOffscreen || - (fiber.flags & Visibility && fiber.memoizedState === null); - if (isInStrictMode && hasOffscreenBecomeVisible) { - disappearLayoutEffects(fiber); - disconnectPassiveEffect(fiber); - reappearLayoutEffects(root, fiber.alternate, fiber, false); - reconnectPassiveEffects(root, fiber, NoLanes, null, false); + if (isInStrictMode && fiber.flags & Visibility) { + // Double invoke effects on Offscreen's subtree only + // if it is visible and its visibility has changed. + doubleInvokeEffectsOnFiber(root, fiber); + } else if (fiber.subtreeFlags & PlacementDEV) { + // Something in the subtree could have been suspended. + // We need to continue traversal and find newly inserted fibers. + recursivelyTraverseAndDoubleInvokeEffectsInDEV( + root, + fiber, + isInStrictMode, + ); } resetCurrentDebugFiberInDEV(); - } else { - recursivelyTraverseAndDoubleInvokeEffectsInDEV(root, fiber, isInStrictMode); } } diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js index 6250330b6e957..07ec677d0aaa2 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js @@ -3211,12 +3211,20 @@ function recursivelyTraverseAndDoubleInvokeEffectsInDEV( } let child = parentFiber.child; while (child !== null) { - doubleInvokeEffectsInDEV(root, child, isInStrictMode); + doubleInvokeEffectsInDEVIfNecessary(root, child, isInStrictMode); child = child.sibling; } } -function doubleInvokeEffectsInDEV( +// Unconditionally disconnects and connects passive and layout effects. +function doubleInvokeEffectsOnFiber(root: FiberRoot, fiber: Fiber) { + disappearLayoutEffects(fiber); + disconnectPassiveEffect(fiber); + reappearLayoutEffects(root, fiber.alternate, fiber, false); + reconnectPassiveEffects(root, fiber, NoLanes, null, false); +} + +function doubleInvokeEffectsInDEVIfNecessary( root: FiberRoot, fiber: Fiber, parentIsInStrictMode: boolean, @@ -3224,22 +3232,45 @@ function doubleInvokeEffectsInDEV( const isStrictModeFiber = fiber.type === REACT_STRICT_MODE_TYPE; const isInStrictMode = parentIsInStrictMode || isStrictModeFiber; - if (fiber.flags & PlacementDEV || fiber.tag === OffscreenComponent) { + // First case: the fiber **is not** of type OffscreenComponent. No + // special rules apply to double invoking effects. + if (fiber.tag !== OffscreenComponent) { + if (fiber.flags & PlacementDEV) { + setCurrentDebugFiberInDEV(fiber); + if (isInStrictMode) { + doubleInvokeEffectsOnFiber(root, fiber); + } + resetCurrentDebugFiberInDEV(); + } else { + recursivelyTraverseAndDoubleInvokeEffectsInDEV( + root, + fiber, + isInStrictMode, + ); + } + return; + } + + // Second case: the fiber **is** of type OffscreenComponent. + // This branch contains cases specific to Offscreen. + if (fiber.memoizedState === null) { + // Only consider Offscreen that is visible. + // TODO (Offscreen) Handle manual mode. setCurrentDebugFiberInDEV(fiber); - const isNotOffscreen = fiber.tag !== OffscreenComponent; - // Checks if Offscreen is being revealed. For all other components, evaluates to true. - const hasOffscreenBecomeVisible = - isNotOffscreen || - (fiber.flags & Visibility && fiber.memoizedState === null); - if (isInStrictMode && hasOffscreenBecomeVisible) { - disappearLayoutEffects(fiber); - disconnectPassiveEffect(fiber); - reappearLayoutEffects(root, fiber.alternate, fiber, false); - reconnectPassiveEffects(root, fiber, NoLanes, null, false); + if (isInStrictMode && fiber.flags & Visibility) { + // Double invoke effects on Offscreen's subtree only + // if it is visible and its visibility has changed. + doubleInvokeEffectsOnFiber(root, fiber); + } else if (fiber.subtreeFlags & PlacementDEV) { + // Something in the subtree could have been suspended. + // We need to continue traversal and find newly inserted fibers. + recursivelyTraverseAndDoubleInvokeEffectsInDEV( + root, + fiber, + isInStrictMode, + ); } resetCurrentDebugFiberInDEV(); - } else { - recursivelyTraverseAndDoubleInvokeEffectsInDEV(root, fiber, isInStrictMode); } } diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreenStrictMode-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreenStrictMode-test.js index e6ab08cef5471..b398032d66917 100644 --- a/packages/react-reconciler/src/__tests__/ReactOffscreenStrictMode-test.js +++ b/packages/react-reconciler/src/__tests__/ReactOffscreenStrictMode-test.js @@ -153,4 +153,77 @@ describe('ReactOffscreenStrictMode', () => { ); }); }); + + // @gate __DEV__ && enableStrictEffects && enableOffscreen + it('should double invoke effects on unsuspended child', async () => { + let shouldSuspend = true; + let resolve; + const suspensePromise = new Promise(_resolve => { + resolve = _resolve; + }); + + function Parent() { + log.push('Parent rendered'); + React.useEffect(() => { + log.push('Parent mount'); + return () => { + log.push('Parent unmount'); + }; + }); + + return ( + + + + ); + } + + function Child() { + log.push('Child rendered'); + React.useEffect(() => { + log.push('Child mount'); + return () => { + log.push('Child unmount'); + }; + }); + if (shouldSuspend) { + log.push('Child suspended'); + throw suspensePromise; + } + return null; + } + + act(() => { + ReactNoop.render( + + + + + , + ); + }); + + log.push('------------------------------'); + + await act(async () => { + resolve(); + shouldSuspend = false; + }); + + expect(log).toEqual([ + 'Parent rendered', + 'Parent rendered', + 'Child rendered', + 'Child suspended', + 'Parent mount', + 'Parent unmount', + 'Parent mount', + '------------------------------', + 'Child rendered', + 'Child rendered', + 'Child mount', + 'Child unmount', + 'Child mount', + ]); + }); });