diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 6d600971d4461..17c104ff0fff9 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -40,6 +40,7 @@ import type { import { alwaysThrottleRetries, enableCreateEventHandleAPI, + enableHiddenSubtreeInsertionEffectCleanup, enablePersistedModeClonedFlag, enableProfilerTimer, enableProfilerCommitHooks, @@ -147,6 +148,7 @@ import { getExecutionContext, CommitContext, NoContext, + setIsRunningInsertionEffect, } from './ReactFiberWorkLoop'; import { NoFlags as NoHookEffect, @@ -1324,7 +1326,78 @@ function commitDeletionEffectsOnFiber( case ForwardRef: case MemoComponent: case SimpleMemoComponent: { - if (!offscreenSubtreeWasHidden) { + if (enableHiddenSubtreeInsertionEffectCleanup) { + // When deleting a fiber, we may need to destroy insertion or layout effects. + // Insertion effects are not destroyed on hidden, only when destroyed, so now + // we need to destroy them. Layout effects are destroyed when hidden, so + // we only need to destroy them if the tree is visible. + const updateQueue: FunctionComponentUpdateQueue | null = + (deletedFiber.updateQueue: any); + if (updateQueue !== null) { + const lastEffect = updateQueue.lastEffect; + if (lastEffect !== null) { + const firstEffect = lastEffect.next; + + let effect = firstEffect; + do { + const tag = effect.tag; + const inst = effect.inst; + const destroy = inst.destroy; + if (destroy !== undefined) { + if ((tag & HookInsertion) !== NoHookEffect) { + // TODO: add insertion effect marks and profiling. + if (__DEV__) { + setIsRunningInsertionEffect(true); + } + + inst.destroy = undefined; + safelyCallDestroy( + deletedFiber, + nearestMountedAncestor, + destroy, + ); + + if (__DEV__) { + setIsRunningInsertionEffect(false); + } + } else if ( + !offscreenSubtreeWasHidden && + (tag & HookLayout) !== NoHookEffect + ) { + // Offscreen fibers already unmounted their layout effects. + // We only need to destroy layout effects for visible trees. + if (enableSchedulingProfiler) { + markComponentLayoutEffectUnmountStarted(deletedFiber); + } + + if (shouldProfile(deletedFiber)) { + startLayoutEffectTimer(); + inst.destroy = undefined; + safelyCallDestroy( + deletedFiber, + nearestMountedAncestor, + destroy, + ); + recordLayoutEffectDuration(deletedFiber); + } else { + inst.destroy = undefined; + safelyCallDestroy( + deletedFiber, + nearestMountedAncestor, + destroy, + ); + } + + if (enableSchedulingProfiler) { + markComponentLayoutEffectUnmountStopped(); + } + } + } + effect = effect.next; + } while (effect !== firstEffect); + } + } + } else if (!offscreenSubtreeWasHidden) { const updateQueue: FunctionComponentUpdateQueue | null = (deletedFiber.updateQueue: any); if (updateQueue !== null) { diff --git a/packages/react-reconciler/src/__tests__/Activity-test.js b/packages/react-reconciler/src/__tests__/Activity-test.js index e174c7c06ca76..cc5c4b921ce98 100644 --- a/packages/react-reconciler/src/__tests__/Activity-test.js +++ b/packages/react-reconciler/src/__tests__/Activity-test.js @@ -7,6 +7,7 @@ let Activity; let useState; let useLayoutEffect; let useEffect; +let useInsertionEffect; let useMemo; let useRef; let startTransition; @@ -25,6 +26,7 @@ describe('Activity', () => { LegacyHidden = React.unstable_LegacyHidden; Activity = React.unstable_Activity; useState = React.useState; + useInsertionEffect = React.useInsertionEffect; useLayoutEffect = React.useLayoutEffect; useEffect = React.useEffect; useMemo = React.useMemo; @@ -43,6 +45,13 @@ describe('Activity', () => { } function LoggedText({text, children}) { + useInsertionEffect(() => { + Scheduler.log(`mount insertion ${text}`); + return () => { + Scheduler.log(`unmount insertion ${text}`); + }; + }); + useEffect(() => { Scheduler.log(`mount ${text}`); return () => { @@ -1436,6 +1445,63 @@ describe('Activity', () => { ); }); + // @gate enableActivity + it('insertion effects are not disconnected when the visibility changes', async () => { + function Child({step}) { + useInsertionEffect(() => { + Scheduler.log(`Commit mount [${step}]`); + return () => { + Scheduler.log(`Commit unmount [${step}]`); + }; + }, [step]); + return ; + } + + function App({show, step}) { + return ( + + {useMemo( + () => ( + + ), + [step], + )} + + ); + } + + const root = ReactNoop.createRoot(); + await act(() => { + root.render(); + }); + assertLog([1, 'Commit mount [1]']); + expect(root).toMatchRenderedOutput(); + + // Hide the tree. This will not unmount insertion effects. + await act(() => { + root.render(); + }); + assertLog([]); + expect(root).toMatchRenderedOutput(