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();
+
+ // Update.
+ await act(() => {
+ root.render();
+ });
+ // The update is pre-rendered so insertion effects are fired
+ assertLog([2, 'Commit unmount [1]', 'Commit mount [2]']);
+ expect(root).toMatchRenderedOutput();
+
+ // Reveal the tree.
+ await act(() => {
+ root.render();
+ });
+ // The update doesn't render because it was already pre-rendered, and the
+ // insertion effect already fired.
+ assertLog([]);
+ expect(root).toMatchRenderedOutput();
+ });
+
describe('manual interactivity', () => {
// @gate enableActivity
it('should attach ref only for mode null', async () => {
@@ -1904,6 +1970,9 @@ describe('Activity', () => {
'outer',
'middle',
'inner',
+ 'mount insertion inner',
+ 'mount insertion middle',
+ 'mount insertion outer',
'mount layout inner',
'mount layout middle',
'mount layout outer',
@@ -1964,6 +2033,22 @@ describe('Activity', () => {
});
assertLog(['unmount layout inner', 'unmount inner']);
+
+ await act(() => {
+ root.render(null);
+ });
+
+ assertLog([
+ 'unmount insertion outer',
+ 'unmount layout outer',
+ 'unmount insertion middle',
+ 'unmount layout middle',
+ ...(gate('enableHiddenSubtreeInsertionEffectCleanup')
+ ? ['unmount insertion inner']
+ : []),
+ 'unmount outer',
+ 'unmount middle',
+ ]);
});
// @gate enableActivity
diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
index 0fc64b39da899..bc87e47083e2a 100644
--- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
@@ -19,6 +19,7 @@ let resolveText;
let ReactNoop;
let Scheduler;
let Suspense;
+let Activity;
let useState;
let useReducer;
let useEffect;
@@ -64,6 +65,7 @@ describe('ReactHooksWithNoopRenderer', () => {
useTransition = React.useTransition;
useDeferredValue = React.useDeferredValue;
Suspense = React.Suspense;
+ Activity = React.unstable_Activity;
ContinuousEventPriority =
require('react-reconciler/constants').ContinuousEventPriority;
if (gate(flags => flags.enableSuspenseList)) {
@@ -2997,6 +2999,57 @@ describe('ReactHooksWithNoopRenderer', () => {
root.render();
});
});
+
+ // @gate enableActivity
+ it('warns when setState is called from offscreen deleted insertion effect cleanup', async () => {
+ function App(props) {
+ const [, setX] = useState(0);
+ useInsertionEffect(() => {
+ if (props.throw) {
+ throw Error('No');
+ }
+ return () => {
+ setX(1);
+ };
+ }, [props.throw, props.foo]);
+ return null;
+ }
+
+ const root = ReactNoop.createRoot();
+ await act(() => {
+ root.render(
+
+
+ ,
+ );
+ });
+
+ if (gate(flags => flags.enableHiddenSubtreeInsertionEffectCleanup)) {
+ await expect(async () => {
+ await act(() => {
+ root.render();
+ });
+ }).toErrorDev(['useInsertionEffect must not schedule updates.']);
+ } else {
+ await expect(async () => {
+ await act(() => {
+ root.render();
+ });
+ }).toErrorDev([]);
+ }
+
+ // Should not warn for regular effects after throw.
+ function NotInsertion() {
+ const [, setX] = useState(0);
+ useEffect(() => {
+ setX(1);
+ }, []);
+ return null;
+ }
+ await act(() => {
+ root.render();
+ });
+ });
});
describe('useLayoutEffect', () => {
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js
index 4a094dc7fed43..d45b29aee59ef 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js
@@ -148,6 +148,13 @@ describe('ReactSuspenseEffectsSemantics', () => {
function Text({children = null, text}) {
Scheduler.log(`Text:${text} render`);
+ React.useInsertionEffect(() => {
+ Scheduler.log(`Text:${text} create insertion`);
+ return () => {
+ Scheduler.log(`Text:${text} destroy insertion`);
+ };
+ }, []);
+
React.useLayoutEffect(() => {
Scheduler.log(`Text:${text} create layout`);
return () => {
@@ -268,6 +275,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Suspend:Async',
'Text:Fallback render',
'Text:Outside render',
+ 'Text:Fallback create insertion',
+ 'Text:Outside create insertion',
'Text:Fallback create layout',
'Text:Outside create layout',
'App create layout',
@@ -298,7 +307,9 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Inside:Before render',
'AsyncText:Async render',
'ClassText:Inside:After render',
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
+ 'Text:Inside:Before create insertion',
'Text:Inside:Before create layout',
'AsyncText:Async create layout',
'ClassText:Inside:After componentDidMount',
@@ -320,9 +331,11 @@ describe('ReactSuspenseEffectsSemantics', () => {
});
assertLog([
'App destroy layout',
+ 'Text:Inside:Before destroy insertion',
'Text:Inside:Before destroy layout',
'AsyncText:Async destroy layout',
'ClassText:Inside:After componentWillUnmount',
+ 'Text:Outside destroy insertion',
'Text:Outside destroy layout',
'App destroy passive',
'Text:Inside:Before destroy passive',
@@ -395,6 +408,9 @@ describe('ReactSuspenseEffectsSemantics', () => {
'ClassText:Inside:After render',
'Text:Fallback render',
'Text:Outside render',
+ 'Text:Inside:Before create insertion',
+ 'Text:Fallback create insertion',
+ 'Text:Outside create insertion',
'Text:Inside:Before create layout',
'ClassText:Inside:After componentDidMount',
'Text:Fallback create layout',
@@ -420,6 +436,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
});
assertLog([
'AsyncText:Async render',
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
'AsyncText:Async create layout',
'Text:Fallback destroy passive',
@@ -439,9 +456,11 @@ describe('ReactSuspenseEffectsSemantics', () => {
});
assertLog([
'App destroy layout',
+ 'Text:Inside:Before destroy insertion',
'Text:Inside:Before destroy layout',
'AsyncText:Async destroy layout',
'ClassText:Inside:After componentWillUnmount',
+ 'Text:Outside destroy insertion',
'Text:Outside destroy layout',
'App destroy passive',
'Text:Inside:Before destroy passive',
@@ -452,7 +471,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
});
});
- describe('layout effects within a tree that re-suspends in an update', () => {
+ describe('effects within a tree that re-suspends in an update', () => {
// @gate enableLegacyCache && !disableLegacyMode
it('should not be destroyed or recreated in legacy roots', async () => {
function App({children = null}) {
@@ -490,6 +509,9 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Inside:Before render',
'Text:Inside:After render',
'Text:Outside render',
+ 'Text:Inside:Before create insertion',
+ 'Text:Inside:After create insertion',
+ 'Text:Outside create insertion',
'Text:Inside:Before create layout',
'Text:Inside:After create layout',
'Text:Outside create layout',
@@ -522,6 +544,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Inside:After render',
'Text:Fallback render',
'Text:Outside render',
+ 'Text:Fallback create insertion',
'Text:Fallback create layout',
'Text:Fallback create passive',
]);
@@ -553,6 +576,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
});
assertLog([
'AsyncText:Async render',
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
'AsyncText:Async create layout',
'Text:Fallback destroy passive',
@@ -572,9 +596,12 @@ describe('ReactSuspenseEffectsSemantics', () => {
});
assertLog([
'App destroy layout',
+ 'Text:Inside:Before destroy insertion',
'Text:Inside:Before destroy layout',
'AsyncText:Async destroy layout',
+ 'Text:Inside:After destroy insertion',
'Text:Inside:After destroy layout',
+ 'Text:Outside destroy insertion',
'Text:Outside destroy layout',
'App destroy passive',
'Text:Inside:Before destroy passive',
@@ -620,6 +647,9 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Inside:Before render',
'Text:Inside:After render',
'Text:Outside render',
+ 'Text:Inside:Before create insertion',
+ 'Text:Inside:After create insertion',
+ 'Text:Outside create insertion',
'Text:Inside:Before create layout',
'Text:Inside:After create layout',
'Text:Outside create layout',
@@ -652,6 +682,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Outside render',
'Text:Inside:Before destroy layout',
'Text:Inside:After destroy layout',
+ 'Text:Fallback create insertion',
'Text:Fallback create layout',
]);
await waitForAll([
@@ -683,6 +714,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Inside:Before render',
'AsyncText:Async render',
'Text:Inside:After render',
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
'Text:Inside:Before create layout',
'AsyncText:Async create layout',
@@ -704,9 +736,12 @@ describe('ReactSuspenseEffectsSemantics', () => {
});
assertLog([
'App destroy layout',
+ 'Text:Inside:Before destroy insertion',
'Text:Inside:Before destroy layout',
'AsyncText:Async destroy layout',
+ 'Text:Inside:After destroy insertion',
'Text:Inside:After destroy layout',
+ 'Text:Outside destroy insertion',
'Text:Outside destroy layout',
'App destroy passive',
'Text:Inside:Before destroy passive',
@@ -893,6 +928,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
'App render',
'Text:Outer render',
'Text:Inner render',
+ 'Text:Inner create insertion',
+ 'Text:Outer create insertion',
'Text:Inner create layout',
'Text:Outer create layout',
'App create layout',
@@ -919,6 +956,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Fallback render',
'Text:Outer destroy layout',
'Text:Inner destroy layout',
+ 'Text:Fallback create insertion',
'Text:Fallback create layout',
]);
await waitForAll([
@@ -946,6 +984,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'AsyncText:Async render',
'Text:Outer render',
'Text:Inner render',
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
'AsyncText:Async create layout',
'Text:Inner create layout',
@@ -968,7 +1007,9 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'App destroy layout',
'AsyncText:Async destroy layout',
+ 'Text:Outer destroy insertion',
'Text:Outer destroy layout',
+ 'Text:Inner destroy insertion',
'Text:Inner destroy layout',
'App destroy passive',
'AsyncText:Async destroy passive',
@@ -1013,6 +1054,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
'App render',
'Text:Outer render',
'Text:MemoizedInner render',
+ 'Text:MemoizedInner create insertion',
+ 'Text:Outer create insertion',
'Text:MemoizedInner create layout',
'Text:Outer create layout',
'App create layout',
@@ -1040,6 +1083,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Fallback render',
'Text:Outer destroy layout',
'Text:MemoizedInner destroy layout',
+ 'Text:Fallback create insertion',
'Text:Fallback create layout',
]);
await waitForAll([
@@ -1066,6 +1110,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'AsyncText:Async render',
'Text:Outer render',
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
'AsyncText:Async create layout',
'Text:MemoizedInner create layout',
@@ -1088,7 +1133,9 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'App destroy layout',
'AsyncText:Async destroy layout',
+ 'Text:Outer destroy insertion',
'Text:Outer destroy layout',
+ 'Text:MemoizedInner destroy insertion',
'Text:MemoizedInner destroy layout',
'App destroy passive',
'AsyncText:Async destroy passive',
@@ -1119,6 +1166,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'Text:Outer render',
'Text:Inner render',
+ 'Text:Outer create insertion',
+ 'Text:Inner create insertion',
'Text:Outer create layout',
'Text:Inner create layout',
'Text:Outer create passive',
@@ -1143,6 +1192,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Suspend:InnerAsync_1',
'Text:InnerFallback render',
'Text:Inner destroy layout',
+ 'Text:InnerFallback create insertion',
'Text:InnerFallback create layout',
'Text:InnerFallback create passive',
@@ -1175,6 +1225,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:OuterFallback render',
'Text:Outer destroy layout',
'Text:InnerFallback destroy layout',
+ 'Text:OuterFallback create insertion',
'Text:OuterFallback create layout',
'Text:OuterFallback create passive',
@@ -1267,7 +1318,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Inner render',
'Suspend:InnerAsync_2',
'Text:InnerFallback render',
-
+ 'Text:OuterFallback destroy insertion',
'Text:OuterFallback destroy layout',
'Text:Outer create layout',
'AsyncText:OuterAsync_1 create layout',
@@ -1295,6 +1346,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'Text:Inner render',
'AsyncText:InnerAsync_2 render',
+ 'Text:InnerFallback destroy insertion',
'Text:InnerFallback destroy layout',
'Text:Inner create layout',
'AsyncText:InnerAsync_2 create layout',
@@ -1327,6 +1379,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'AsyncText:OuterAsync_1 destroy layout',
'Text:Inner destroy layout',
'AsyncText:InnerAsync_2 destroy layout',
+ 'Text:OuterFallback create insertion',
'Text:OuterFallback create layout',
'Text:OuterFallback create passive',
@@ -1358,6 +1411,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'AsyncText:OuterAsync_2 render',
'Text:Inner render',
'AsyncText:InnerAsync_2 render',
+ 'Text:OuterFallback destroy insertion',
'Text:OuterFallback destroy layout',
'Text:Outer create layout',
'AsyncText:OuterAsync_2 create layout',
@@ -1397,6 +1451,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'Text:Outer render',
'Text:Inner render',
+ 'Text:Outer create insertion',
+ 'Text:Inner create insertion',
'Text:Outer create layout',
'Text:Inner create layout',
'Text:Outer create passive',
@@ -1421,6 +1477,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Suspend:InnerAsync_1',
'Text:InnerFallback render',
'Text:Inner destroy layout',
+ 'Text:InnerFallback create insertion',
'Text:InnerFallback create layout',
'Text:InnerFallback create passive',
@@ -1452,6 +1509,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:OuterFallback render',
'Text:Outer destroy layout',
'Text:InnerFallback destroy layout',
+ 'Text:OuterFallback create insertion',
'Text:OuterFallback create layout',
'Text:OuterFallback create passive',
@@ -1484,7 +1542,11 @@ describe('ReactSuspenseEffectsSemantics', () => {
'AsyncText:OuterAsync_1 render',
'Text:Inner render',
'AsyncText:InnerAsync_1 render',
+ 'Text:OuterFallback destroy insertion',
'Text:OuterFallback destroy layout',
+ ...(gate(flags => flags.enableHiddenSubtreeInsertionEffectCleanup)
+ ? ['Text:InnerFallback destroy insertion']
+ : []),
'Text:Outer create layout',
'AsyncText:OuterAsync_1 create layout',
'Text:Inner create layout',
@@ -1534,6 +1596,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'Text:Inside render',
'Text:Outside render',
+ 'Text:Inside create insertion',
+ 'Text:Outside create insertion',
'Text:Inside create layout',
'Text:Outside create layout',
'Text:Inside create passive',
@@ -1558,6 +1622,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Fallback:Outside render',
'Text:Outside render',
'Text:Inside destroy layout',
+ 'Text:Fallback:Inside create insertion',
+ 'Text:Fallback:Outside create insertion',
'Text:Fallback:Inside create layout',
'Text:Fallback:Outside create layout',
]);
@@ -1596,6 +1662,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Fallback:Outside render',
'Text:Outside render',
'Text:Fallback:Inside destroy layout',
+ 'Text:Fallback:Fallback create insertion',
'Text:Fallback:Fallback create layout',
]);
await waitForAll([
@@ -1629,7 +1696,12 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'Text:Inside render',
'AsyncText:OutsideAsync render',
+ ...(gate(flags => flags.enableHiddenSubtreeInsertionEffectCleanup)
+ ? ['Text:Fallback:Inside destroy insertion']
+ : []),
+ 'Text:Fallback:Fallback destroy insertion',
'Text:Fallback:Fallback destroy layout',
+ 'Text:Fallback:Outside destroy insertion',
'Text:Fallback:Outside destroy layout',
'Text:Inside create layout',
'AsyncText:OutsideAsync create layout',
@@ -1677,6 +1749,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'Text:Inside render',
'Text:Outside render',
+ 'Text:Inside create insertion',
+ 'Text:Outside create insertion',
'Text:Inside create layout',
'Text:Outside create layout',
'Text:Inside create passive',
@@ -1707,6 +1781,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Fallback:Outside render',
'Text:Outside render',
'Text:Inside destroy layout',
+ 'Text:Fallback:Fallback create insertion',
+ 'Text:Fallback:Outside create insertion',
'Text:Fallback:Fallback create layout',
'Text:Fallback:Outside create layout',
'Text:Fallback:Fallback create passive',
@@ -1737,7 +1813,9 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'Text:Fallback:Inside render',
'AsyncText:FallbackAsync render',
+ 'Text:Fallback:Fallback destroy insertion',
'Text:Fallback:Fallback destroy layout',
+ 'Text:Fallback:Inside create insertion',
'Text:Fallback:Inside create layout',
'AsyncText:FallbackAsync create layout',
'Text:Fallback:Fallback destroy passive',
@@ -1761,8 +1839,10 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'Text:Inside render',
'AsyncText:OutsideAsync render',
+ 'Text:Fallback:Inside destroy insertion',
'Text:Fallback:Inside destroy layout',
'AsyncText:FallbackAsync destroy layout',
+ 'Text:Fallback:Outside destroy insertion',
'Text:Fallback:Outside destroy layout',
'Text:Inside create layout',
'AsyncText:OutsideAsync create layout',
@@ -1807,6 +1887,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'Text:Inside render',
'Text:Outside render',
+ 'Text:Inside create insertion',
+ 'Text:Outside create insertion',
'Text:Inside create layout',
'Text:Outside create layout',
'Text:Inside create passive',
@@ -1828,6 +1910,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Fallback render',
'Text:Outside render',
'Text:Inside destroy layout',
+ 'Text:Fallback create insertion',
'Text:Fallback create layout',
]);
await waitForAll([
@@ -1850,6 +1933,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
});
assertLog([
'Text:Inside render',
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
'Text:Inside create layout',
'Text:Fallback destroy passive',
@@ -1916,6 +2000,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
'ThrowsInDidMount render',
'Text:Inside render',
'Text:Outside render',
+ 'Text:Inside create insertion',
+ 'Text:Outside create insertion',
'ThrowsInDidMount componentDidMount',
'Text:Inside create layout',
'Text:Outside create layout',
@@ -1949,6 +2035,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Outside render',
'ThrowsInDidMount componentWillUnmount',
'Text:Inside destroy layout',
+ 'Text:Fallback create insertion',
'Text:Fallback create layout',
'Text:Fallback create passive',
@@ -1974,6 +2061,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'AsyncText:Async render',
'ThrowsInDidMount render',
'Text:Inside render',
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
'AsyncText:Async create layout',
@@ -1986,11 +2074,13 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Fallback destroy passive',
'AsyncText:Async create passive',
- // Destroy layout and passive effects in the errored tree.
+ // Destroy insertion, layout, and passive effects in the errored tree.
'App destroy layout',
'AsyncText:Async destroy layout',
'ThrowsInDidMount componentWillUnmount',
+ 'Text:Inside destroy insertion',
'Text:Inside destroy layout',
+ 'Text:Outside destroy insertion',
'Text:Outside destroy layout',
'AsyncText:Async destroy passive',
'Text:Inside destroy passive',
@@ -1999,6 +2089,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
// Render fallback
'ErrorBoundary render: catch',
'Text:Error render',
+ 'Text:Error create insertion',
'Text:Error create layout',
'Text:Error create passive',
]);
@@ -2054,6 +2145,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
'ThrowsInWillUnmount render',
'Text:Inside render',
'Text:Outside render',
+ 'Text:Inside create insertion',
+ 'Text:Outside create insertion',
'ThrowsInWillUnmount componentDidMount',
'Text:Inside create layout',
'Text:Outside create layout',
@@ -2092,12 +2185,18 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Inside destroy layout',
// Finish the in-progress commit
+ 'Text:Fallback create insertion',
'Text:Fallback create layout',
'Text:Fallback create passive',
// Destroy layout and passive effects in the errored tree.
'App destroy layout',
+ ...(gate(flags => flags.enableHiddenSubtreeInsertionEffectCleanup)
+ ? ['Text:Inside destroy insertion']
+ : []),
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
+ 'Text:Outside destroy insertion',
'Text:Outside destroy layout',
'Text:Inside destroy passive',
'Text:Fallback destroy passive',
@@ -2106,6 +2205,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
// Render fallback
'ErrorBoundary render: catch',
'Text:Error render',
+ 'Text:Error create insertion',
'Text:Error create layout',
'Text:Error create passive',
]);
@@ -2163,6 +2263,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
'ThrowsInLayoutEffect render',
'Text:Inside render',
'Text:Outside render',
+ 'Text:Inside create insertion',
+ 'Text:Outside create insertion',
'ThrowsInLayoutEffect useLayoutEffect create',
'Text:Inside create layout',
'Text:Outside create layout',
@@ -2196,6 +2298,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Outside render',
'ThrowsInLayoutEffect useLayoutEffect destroy',
'Text:Inside destroy layout',
+ 'Text:Fallback create insertion',
'Text:Fallback create layout',
'Text:Fallback create passive',
@@ -2226,6 +2329,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'ThrowsInLayoutEffect render',
'Text:Inside render',
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
// Even though an error was thrown in useLayoutEffect,
@@ -2241,7 +2345,9 @@ describe('ReactSuspenseEffectsSemantics', () => {
// Destroy layout and passive effects in the errored tree.
'App destroy layout',
'AsyncText:Async destroy layout',
+ 'Text:Inside destroy insertion',
'Text:Inside destroy layout',
+ 'Text:Outside destroy insertion',
'Text:Outside destroy layout',
'AsyncText:Async destroy passive',
'Text:Inside destroy passive',
@@ -2250,6 +2356,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
// Render fallback
'ErrorBoundary render: catch',
'Text:Error render',
+ 'Text:Error create insertion',
'Text:Error create layout',
'Text:Error create passive',
]);
@@ -2305,6 +2412,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
'ThrowsInLayoutEffectDestroy render',
'Text:Inside render',
'Text:Outside render',
+ 'Text:Inside create insertion',
+ 'Text:Outside create insertion',
'ThrowsInLayoutEffectDestroy useLayoutEffect create',
'Text:Inside create layout',
'Text:Outside create layout',
@@ -2343,12 +2452,18 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Inside destroy layout',
// Finish the in-progress commit
+ 'Text:Fallback create insertion',
'Text:Fallback create layout',
'Text:Fallback create passive',
// Destroy layout and passive effects in the errored tree.
'App destroy layout',
+ ...(gate(flags => flags.enableHiddenSubtreeInsertionEffectCleanup)
+ ? ['Text:Inside destroy insertion']
+ : []),
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
+ 'Text:Outside destroy insertion',
'Text:Outside destroy layout',
'Text:Inside destroy passive',
'Text:Fallback destroy passive',
@@ -2357,6 +2472,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
// Render fallback
'ErrorBoundary render: catch',
'Text:Error render',
+ 'Text:Error create insertion',
'Text:Error create layout',
'Text:Error create passive',
]);
@@ -2402,6 +2518,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'Text:Function render',
'ClassText:Class render',
+ 'Text:Function create insertion',
'Text:Function create layout',
'ClassText:Class componentDidMount',
'Text:Function create passive',
@@ -2503,6 +2620,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
ReactNoop.render(null);
});
assertLog([
+ 'Text:Function destroy insertion',
'Text:Function destroy layout',
'AsyncText:Async_1 destroy layout',
'AsyncText:Async_2 destroy layout',
@@ -2562,6 +2680,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Function render',
'Suspender "null" render',
'ClassText:Class render',
+ 'Text:Function create insertion',
'Text:Function create layout',
'ClassText:Class componentDidMount',
'Text:Function create passive',
@@ -2657,6 +2776,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
ReactNoop.render(null);
});
assertLog([
+ 'Text:Function destroy insertion',
'Text:Function destroy layout',
'ClassText:Class componentWillUnmount',
'Text:Function destroy passive',
@@ -2774,6 +2894,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'ClassComponent:refCallback render',
'RefCheckerInner:refCallback render',
'Text:Fallback render',
+ 'Text:Fallback create insertion',
'Text:Fallback create layout',
'Text:Fallback create passive',
]);
@@ -2785,6 +2906,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
});
assertLog([
'AsyncText:Async render',
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
'AsyncText:Async create layout',
'Text:Fallback destroy passive',
@@ -2852,6 +2974,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'RefCheckerInner:refObject destroy layout ref? false',
'RefCheckerOuter refCallback value? false',
'RefCheckerInner:refCallback destroy layout ref? false',
+ 'Text:Fallback create insertion',
'Text:Fallback create layout',
'Text:Fallback create passive',
@@ -2881,6 +3004,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'RefCheckerOuter render',
'RefCheckerInner:refObject render',
'RefCheckerInner:refCallback render',
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
'AsyncText:Async create layout',
'RefCheckerInner:refObject create layout ref? false',
@@ -2962,6 +3086,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'RefCheckerInner:refObject destroy layout ref? false',
'RefCheckerOuter refCallback value? false',
'RefCheckerInner:refCallback destroy layout ref? false',
+ 'Text:Fallback create insertion',
'Text:Fallback create layout',
'Text:Fallback create passive',
@@ -2989,6 +3114,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'RefCheckerInner:refObject render',
'ClassComponent:refCallback render',
'RefCheckerInner:refCallback render',
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
'AsyncText:Async create layout',
'RefCheckerInner:refObject create layout ref? false',
@@ -3070,6 +3196,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'RefCheckerInner:refObject destroy layout ref? false',
'RefCheckerOuter refCallback value? false',
'RefCheckerInner:refCallback destroy layout ref? false',
+ 'Text:Fallback create insertion',
'Text:Fallback create layout',
'Text:Fallback create passive',
@@ -3097,6 +3224,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'RefCheckerInner:refObject render',
'FunctionComponent render',
'RefCheckerInner:refCallback render',
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
'AsyncText:Async create layout',
'RefCheckerInner:refObject create layout ref? false',
@@ -3180,6 +3308,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Suspend:Async',
'Text:Fallback render',
'RefChecker destroy layout ref? true',
+ 'Text:Fallback create insertion',
'Text:Fallback create layout',
'Text:Fallback create passive',
@@ -3196,6 +3325,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
assertLog([
'AsyncText:Async render',
'RefChecker render',
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
'AsyncText:Async create layout',
'RefChecker create layout ref? true',
@@ -3265,6 +3395,8 @@ describe('ReactSuspenseEffectsSemantics', () => {
'ThrowsInRefCallback render',
'Text:Inside render',
'Text:Outside render',
+ 'Text:Inside create insertion',
+ 'Text:Outside create insertion',
'ThrowsInRefCallback refCallback ref? true',
'Text:Inside create layout',
'Text:Outside create layout',
@@ -3298,6 +3430,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Outside render',
'ThrowsInRefCallback refCallback ref? false',
'Text:Inside destroy layout',
+ 'Text:Fallback create insertion',
'Text:Fallback create layout',
'Text:Fallback create passive',
@@ -3330,6 +3463,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
// Even though an error was thrown in refCallback,
// subsequent layout effects should still be created.
+ 'Text:Fallback destroy insertion',
'Text:Fallback destroy layout',
'AsyncText:Async create layout',
'ThrowsInRefCallback refCallback ref? true',
@@ -3339,11 +3473,13 @@ describe('ReactSuspenseEffectsSemantics', () => {
'Text:Fallback destroy passive',
'AsyncText:Async create passive',
- // Destroy layout and passive effects in the errored tree.
+ // Destroy insertion, layout, and passive effects in the errored tree.
'App destroy layout',
'AsyncText:Async destroy layout',
'ThrowsInRefCallback refCallback ref? false',
+ 'Text:Inside destroy insertion',
'Text:Inside destroy layout',
+ 'Text:Outside destroy insertion',
'Text:Outside destroy layout',
'AsyncText:Async destroy passive',
'Text:Inside destroy passive',
@@ -3352,6 +3488,7 @@ describe('ReactSuspenseEffectsSemantics', () => {
// Render fallback
'ErrorBoundary render: catch',
'Text:Error render',
+ 'Text:Error create insertion',
'Text:Error create layout',
'Text:Error create passive',
]);
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index a9ff23eb8d28f..3331deded9af9 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -172,6 +172,11 @@ export const transitionLaneExpirationMs = 5000;
// Renames the internal symbol for elements since they have changed signature/constructor
export const renameElementSymbol = true;
+/**
+ * Enables a fix to run insertion effect cleanup on hidden subtrees.
+ */
+export const enableHiddenSubtreeInsertionEffectCleanup = false;
+
/**
* Removes legacy style context defined using static `contextTypes` and consumed with static `childContextTypes`.
*/
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js
index 2d49c56377a2f..2cae164dfdea5 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb-dynamic.js
@@ -20,6 +20,7 @@
export const alwaysThrottleRetries = __VARIANT__;
export const enableAddPropertiesFastPath = __VARIANT__;
export const enableObjectFiber = __VARIANT__;
+export const enableHiddenSubtreeInsertionEffectCleanup = __VARIANT__;
export const enablePersistedModeClonedFlag = __VARIANT__;
export const enableShallowPropDiffing = __VARIANT__;
export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js
index 61138ddbee6c1..a6001a9559609 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js
@@ -22,6 +22,7 @@ export const {
alwaysThrottleRetries,
enableAddPropertiesFastPath,
enableFabricCompleteRootInCommitPhase,
+ enableHiddenSubtreeInsertionEffectCleanup,
enableObjectFiber,
enablePersistedModeClonedFlag,
enableShallowPropDiffing,
diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js
index 4e50442c2579f..723cc06c33a27 100644
--- a/packages/shared/forks/ReactFeatureFlags.native-oss.js
+++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js
@@ -50,6 +50,7 @@ export const enableFizzExternalRuntime = true;
export const enableFlightReadableStream = true;
export const enableGetInspectorDataForInstanceInProduction = false;
export const enableHalt = false;
+export const enableHiddenSubtreeInsertionEffectCleanup = false;
export const enableInfiniteRenderLoopDetection = true;
export const enableLazyContextPropagation = false;
export const enableContextProfiling = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
index 53a662cffeb1a..3581d31831784 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js
@@ -46,6 +46,7 @@ export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = true;
export const enableGetInspectorDataForInstanceInProduction = false;
export const enableFabricCompleteRootInCommitPhase = false;
+export const enableHiddenSubtreeInsertionEffectCleanup = false;
export const enableRetryLaneExpiration = false;
export const retryLaneExpirationMs = 5000;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
index ebc3ddab2a551..edf67adf18bc3 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
@@ -44,6 +44,7 @@ export const enableHalt = false;
export const enableInfiniteRenderLoopDetection = true;
export const enableLazyContextPropagation = false;
export const enableContextProfiling = false;
+export const enableHiddenSubtreeInsertionEffectCleanup = true;
export const enableLegacyCache = false;
export const enableLegacyFBSupport = false;
export const enableLegacyHidden = false;
diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
index a378ab3edf17c..e64904005e143 100644
--- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
@@ -49,6 +49,7 @@ export const enableFilterEmptyStringAttributesDOM = true;
export const enableGetInspectorDataForInstanceInProduction = false;
export const enableRenderableContext = false;
export const enableFabricCompleteRootInCommitPhase = false;
+export const enableHiddenSubtreeInsertionEffectCleanup = true;
export const enableRetryLaneExpiration = false;
export const retryLaneExpirationMs = 5000;
diff --git a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
index d6e11f92649ba..dbd00f671d84e 100644
--- a/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+++ b/packages/shared/forks/ReactFeatureFlags.www-dynamic.js
@@ -21,6 +21,7 @@ export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
export const enableAddPropertiesFastPath = __VARIANT__;
export const enableDeferRootSchedulingToMicrotask = __VARIANT__;
export const enableDO_NOT_USE_disableStrictPassiveEffect = __VARIANT__;
+export const enableHiddenSubtreeInsertionEffectCleanup = __VARIANT__;
export const enableNoCloningMemoCache = __VARIANT__;
export const enableObjectFiber = __VARIANT__;
export const enableRenderableContext = __VARIANT__;
diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js
index e207dbb71b669..e6025242d4a78 100644
--- a/packages/shared/forks/ReactFeatureFlags.www.js
+++ b/packages/shared/forks/ReactFeatureFlags.www.js
@@ -30,6 +30,7 @@ export const {
enableRetryLaneExpiration,
enableTransitionTracing,
enableTrustedTypesIntegration,
+ enableHiddenSubtreeInsertionEffectCleanup,
favorSafetyOverHydrationPerf,
renameElementSymbol,
retryLaneExpirationMs,