From 905106cb35bf9716dffe1eb38fcaa9809b4448b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 24 May 2023 10:05:54 +0200 Subject: [PATCH] Destroy insertion effects when deleting previously hidden subtrees --- .../src/ReactFiberCommitWork.js | 87 ++-- .../ReactSuspenseEffectsSemantics-test.js | 427 ++++++++++++++++++ .../ReactSuspenseEffectsSemanticsDOM-test.js | 42 +- 3 files changed, 502 insertions(+), 54 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js index 1960a4707e244..0aa0f24756fa2 100644 --- a/packages/react-reconciler/src/ReactFiberCommitWork.js +++ b/packages/react-reconciler/src/ReactFiberCommitWork.js @@ -2182,58 +2182,59 @@ function commitDeletionEffectsOnFiber( case ForwardRef: case MemoComponent: case SimpleMemoComponent: { - if (!offscreenSubtreeWasHidden) { - 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) { + 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) { + inst.destroy = undefined; + safelyCallDestroy( + deletedFiber, + nearestMountedAncestor, + destroy, + ); + } else if ( + !offscreenSubtreeWasHidden && + (tag & HookLayout) !== NoHookEffect + ) { + if (enableSchedulingProfiler) { + markComponentLayoutEffectUnmountStarted(deletedFiber); + } + + if (shouldProfile(deletedFiber)) { + startLayoutEffectTimer(); inst.destroy = undefined; safelyCallDestroy( deletedFiber, nearestMountedAncestor, destroy, ); - } else if ((tag & HookLayout) !== NoHookEffect) { - 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, - ); - } + recordLayoutEffectDuration(deletedFiber); + } else { + inst.destroy = undefined; + safelyCallDestroy( + deletedFiber, + nearestMountedAncestor, + destroy, + ); + } - if (enableSchedulingProfiler) { - markComponentLayoutEffectUnmountStopped(); - } + if (enableSchedulingProfiler) { + markComponentLayoutEffectUnmountStopped(); } } - effect = effect.next; - } while (effect !== firstEffect); - } + } + effect = effect.next; + } while (effect !== firstEffect); } } diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js index 5200635b6fca4..3a2bc461ccc76 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js @@ -148,6 +148,12 @@ 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 () => { @@ -166,6 +172,12 @@ describe('ReactSuspenseEffectsSemantics', () => { function AsyncText({children = null, text}) { readText(text); Scheduler.log(`AsyncText:${text} render`); + React.useInsertionEffect(() => { + Scheduler.log(`AsyncText:${text} create insertion`); + return () => { + Scheduler.log(`AsyncText:${text} destroy insertion`); + }; + }, []); React.useLayoutEffect(() => { Scheduler.log(`AsyncText:${text} create layout`); return () => { @@ -230,6 +242,12 @@ describe('ReactSuspenseEffectsSemantics', () => { function App({children = null}) { Scheduler.log('App render'); + React.useInsertionEffect(() => { + Scheduler.log('App create insertion'); + return () => { + Scheduler.log('App destroy insertion'); + }; + }, []); React.useLayoutEffect(() => { Scheduler.log('App create layout'); return () => { @@ -268,6 +286,9 @@ describe('ReactSuspenseEffectsSemantics', () => { 'Suspend:Async', 'Text:Fallback render', 'Text:Outside render', + 'Text:Fallback create insertion', + 'Text:Outside create insertion', + 'App create insertion', 'Text:Fallback create layout', 'Text:Outside create layout', 'App create layout', @@ -290,7 +311,10 @@ 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', + 'AsyncText:Async create insertion', 'Text:Inside:Before create layout', 'AsyncText:Async create layout', 'ClassText:Inside:After componentDidMount', @@ -311,10 +335,14 @@ describe('ReactSuspenseEffectsSemantics', () => { ReactNoop.render(null); }); assertLog([ + 'App destroy insertion', 'App destroy layout', + 'Text:Inside:Before destroy insertion', 'Text:Inside:Before destroy layout', + 'AsyncText:Async destroy insertion', 'AsyncText:Async destroy layout', 'ClassText:Inside:After componentWillUnmount', + 'Text:Outside destroy insertion', 'Text:Outside destroy layout', 'App destroy passive', 'Text:Inside:Before destroy passive', @@ -348,6 +376,12 @@ describe('ReactSuspenseEffectsSemantics', () => { function App({children = null}) { Scheduler.log('App render'); + React.useInsertionEffect(() => { + Scheduler.log('App create insertion'); + return () => { + Scheduler.log('App destroy insertion'); + }; + }, []); React.useLayoutEffect(() => { Scheduler.log('App create layout'); return () => { @@ -387,6 +421,10 @@ 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', + 'App create insertion', 'Text:Inside:Before create layout', 'ClassText:Inside:After componentDidMount', 'Text:Fallback create layout', @@ -412,7 +450,9 @@ describe('ReactSuspenseEffectsSemantics', () => { }); assertLog([ 'AsyncText:Async render', + 'Text:Fallback destroy insertion', 'Text:Fallback destroy layout', + 'AsyncText:Async create insertion', 'AsyncText:Async create layout', 'Text:Fallback destroy passive', 'AsyncText:Async create passive', @@ -430,10 +470,14 @@ describe('ReactSuspenseEffectsSemantics', () => { ReactNoop.renderLegacySyncRoot(null); }); assertLog([ + 'App destroy insertion', 'App destroy layout', + 'Text:Inside:Before destroy insertion', 'Text:Inside:Before destroy layout', + 'AsyncText:Async destroy insertion', 'AsyncText:Async destroy layout', 'ClassText:Inside:After componentWillUnmount', + 'Text:Outside destroy insertion', 'Text:Outside destroy layout', 'App destroy passive', 'Text:Inside:Before destroy passive', @@ -449,6 +493,12 @@ describe('ReactSuspenseEffectsSemantics', () => { it('should not be destroyed or recreated in legacy roots', async () => { function App({children = null}) { Scheduler.log('App render'); + React.useInsertionEffect(() => { + Scheduler.log('App create insertion'); + return () => { + Scheduler.log('App destroy insertion'); + }; + }, []); React.useLayoutEffect(() => { Scheduler.log('App create layout'); return () => { @@ -482,6 +532,10 @@ 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', + 'App create insertion', 'Text:Inside:Before create layout', 'Text:Inside:After create layout', 'Text:Outside create layout', @@ -514,6 +568,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', ]); @@ -545,7 +600,9 @@ describe('ReactSuspenseEffectsSemantics', () => { }); assertLog([ 'AsyncText:Async render', + 'Text:Fallback destroy insertion', 'Text:Fallback destroy layout', + 'AsyncText:Async create insertion', 'AsyncText:Async create layout', 'Text:Fallback destroy passive', 'AsyncText:Async create passive', @@ -563,10 +620,15 @@ describe('ReactSuspenseEffectsSemantics', () => { ReactNoop.renderLegacySyncRoot(null); }); assertLog([ + 'App destroy insertion', 'App destroy layout', + 'Text:Inside:Before destroy insertion', 'Text:Inside:Before destroy layout', + 'AsyncText:Async destroy insertion', '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', @@ -580,6 +642,12 @@ describe('ReactSuspenseEffectsSemantics', () => { it('should be destroyed and recreated for function components', async () => { function App({children = null}) { Scheduler.log('App render'); + React.useInsertionEffect(() => { + Scheduler.log('App create insertion'); + return () => { + Scheduler.log('App destroy insertion'); + }; + }, []); React.useLayoutEffect(() => { Scheduler.log('App create layout'); return () => { @@ -612,6 +680,10 @@ 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', + 'App create insertion', 'Text:Inside:Before create layout', 'Text:Inside:After create layout', 'Text:Outside create layout', @@ -644,6 +716,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(['Text:Fallback create passive']); @@ -665,7 +738,9 @@ describe('ReactSuspenseEffectsSemantics', () => { 'Text:Inside:Before render', 'AsyncText:Async render', 'Text:Inside:After render', + 'Text:Fallback destroy insertion', 'Text:Fallback destroy layout', + 'AsyncText:Async create insertion', 'Text:Inside:Before create layout', 'AsyncText:Async create layout', 'Text:Inside:After create layout', @@ -685,10 +760,15 @@ describe('ReactSuspenseEffectsSemantics', () => { ReactNoop.render(null); }); assertLog([ + 'App destroy insertion', 'App destroy layout', + 'Text:Inside:Before destroy insertion', 'Text:Inside:Before destroy layout', + 'AsyncText:Async destroy insertion', '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', @@ -722,6 +802,12 @@ describe('ReactSuspenseEffectsSemantics', () => { function App({children = null}) { Scheduler.log('App render'); + React.useInsertionEffect(() => { + Scheduler.log('App create insertion'); + return () => { + Scheduler.log('App destroy insertion'); + }; + }, []); React.useLayoutEffect(() => { Scheduler.log('App create layout'); return () => { @@ -755,6 +841,7 @@ describe('ReactSuspenseEffectsSemantics', () => { 'ClassText:Inside:Before render', 'ClassText:Inside:After render', 'ClassText:Outside render', + 'App create insertion', 'ClassText:Inside:Before componentDidMount', 'ClassText:Inside:After componentDidMount', 'ClassText:Outside componentDidMount', @@ -807,6 +894,7 @@ describe('ReactSuspenseEffectsSemantics', () => { 'AsyncText:Async render', 'ClassText:Inside:After render', 'ClassText:Fallback componentWillUnmount', + 'AsyncText:Async create insertion', 'ClassText:Inside:Before componentDidMount', 'AsyncText:Async create layout', 'ClassText:Inside:After componentDidMount', @@ -824,8 +912,10 @@ describe('ReactSuspenseEffectsSemantics', () => { ReactNoop.render(null); }); assertLog([ + 'App destroy insertion', 'App destroy layout', 'ClassText:Inside:Before componentWillUnmount', + 'AsyncText:Async destroy insertion', 'AsyncText:Async destroy layout', 'ClassText:Inside:After componentWillUnmount', 'ClassText:Outside componentWillUnmount', @@ -838,6 +928,12 @@ describe('ReactSuspenseEffectsSemantics', () => { it('should be destroyed and recreated when nested below host components', async () => { function App({children = null}) { Scheduler.log('App render'); + React.useInsertionEffect(() => { + Scheduler.log('App create insertion'); + return () => { + Scheduler.log('App destroy insertion'); + }; + }, []); React.useLayoutEffect(() => { Scheduler.log('App create layout'); return () => { @@ -868,6 +964,9 @@ describe('ReactSuspenseEffectsSemantics', () => { 'App render', 'Text:Outer render', 'Text:Inner render', + 'Text:Inner create insertion', + 'Text:Outer create insertion', + 'App create insertion', 'Text:Inner create layout', 'Text:Outer create layout', 'App create layout', @@ -894,6 +993,7 @@ describe('ReactSuspenseEffectsSemantics', () => { 'Text:Fallback render', 'Text:Outer destroy layout', 'Text:Inner destroy layout', + 'Text:Fallback create insertion', 'Text:Fallback create layout', ]); await waitForAll(['Text:Fallback create passive']); @@ -915,7 +1015,9 @@ describe('ReactSuspenseEffectsSemantics', () => { 'AsyncText:Async render', 'Text:Outer render', 'Text:Inner render', + 'Text:Fallback destroy insertion', 'Text:Fallback destroy layout', + 'AsyncText:Async create insertion', 'AsyncText:Async create layout', 'Text:Inner create layout', 'Text:Outer create layout', @@ -935,9 +1037,13 @@ describe('ReactSuspenseEffectsSemantics', () => { ReactNoop.render(null); }); assertLog([ + 'App destroy insertion', 'App destroy layout', + 'AsyncText:Async destroy insertion', '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', @@ -952,6 +1058,12 @@ describe('ReactSuspenseEffectsSemantics', () => { function App({children = null}) { Scheduler.log('App render'); + React.useInsertionEffect(() => { + Scheduler.log('App create insertion'); + return () => { + Scheduler.log('App destroy insertion'); + }; + }, []); React.useLayoutEffect(() => { Scheduler.log('App create layout'); return () => { @@ -982,6 +1094,9 @@ describe('ReactSuspenseEffectsSemantics', () => { 'App render', 'Text:Outer render', 'Text:MemoizedInner render', + 'Text:MemoizedInner create insertion', + 'Text:Outer create insertion', + 'App create insertion', 'Text:MemoizedInner create layout', 'Text:Outer create layout', 'App create layout', @@ -1009,6 +1124,7 @@ describe('ReactSuspenseEffectsSemantics', () => { 'Text:Fallback render', 'Text:Outer destroy layout', 'Text:MemoizedInner destroy layout', + 'Text:Fallback create insertion', 'Text:Fallback create layout', ]); await waitForAll(['Text:Fallback create passive']); @@ -1029,7 +1145,9 @@ describe('ReactSuspenseEffectsSemantics', () => { assertLog([ 'AsyncText:Async render', 'Text:Outer render', + 'Text:Fallback destroy insertion', 'Text:Fallback destroy layout', + 'AsyncText:Async create insertion', 'AsyncText:Async create layout', 'Text:MemoizedInner create layout', 'Text:Outer create layout', @@ -1049,9 +1167,13 @@ describe('ReactSuspenseEffectsSemantics', () => { ReactNoop.render(null); }); assertLog([ + 'App destroy insertion', 'App destroy layout', + 'AsyncText:Async destroy insertion', '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', @@ -1082,6 +1204,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', @@ -1106,6 +1230,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', ]); @@ -1134,6 +1259,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', ]); @@ -1194,7 +1320,9 @@ describe('ReactSuspenseEffectsSemantics', () => { 'Text:Inner render', 'Suspend:InnerAsync_2', 'Text:InnerFallback render', + 'Text:OuterFallback destroy insertion', 'Text:OuterFallback destroy layout', + 'AsyncText:OuterAsync_1 create insertion', 'Text:Outer create layout', 'AsyncText:OuterAsync_1 create layout', 'Text:InnerFallback create layout', @@ -1217,7 +1345,9 @@ describe('ReactSuspenseEffectsSemantics', () => { assertLog([ 'Text:Inner render', 'AsyncText:InnerAsync_2 render', + 'Text:InnerFallback destroy insertion', 'Text:InnerFallback destroy layout', + 'AsyncText:InnerAsync_2 create insertion', 'Text:Inner create layout', 'AsyncText:InnerAsync_2 create layout', 'Text:InnerFallback destroy passive', @@ -1249,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', ]); @@ -1271,6 +1402,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', @@ -1310,6 +1442,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', @@ -1334,6 +1468,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', ]); @@ -1361,6 +1496,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', ]); @@ -1383,7 +1519,11 @@ describe('ReactSuspenseEffectsSemantics', () => { 'AsyncText:OuterAsync_1 render', 'Text:Inner render', 'AsyncText:InnerAsync_1 render', + 'Text:OuterFallback destroy insertion', 'Text:OuterFallback destroy layout', + 'AsyncText:OuterAsync_1 create insertion', + 'Text:InnerFallback destroy insertion', + 'AsyncText:InnerAsync_1 create insertion', 'Text:Outer create layout', 'AsyncText:OuterAsync_1 create layout', 'Text:Inner create layout', @@ -1433,6 +1573,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', @@ -1457,6 +1599,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', ]); @@ -1491,6 +1635,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(['Text:Fallback:Fallback create passive']); @@ -1513,8 +1658,12 @@ describe('ReactSuspenseEffectsSemantics', () => { assertLog([ 'Text:Inside render', 'AsyncText:OutsideAsync render', + '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', + 'AsyncText:OutsideAsync create insertion', 'Text:Inside create layout', 'AsyncText:OutsideAsync create layout', 'Text:Fallback:Inside destroy passive', @@ -1561,6 +1710,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', @@ -1591,6 +1742,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', @@ -1612,7 +1765,10 @@ 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', + 'AsyncText:FallbackAsync create insertion', 'Text:Fallback:Inside create layout', 'AsyncText:FallbackAsync create layout', 'Text:Fallback:Fallback destroy passive', @@ -1636,9 +1792,13 @@ describe('ReactSuspenseEffectsSemantics', () => { assertLog([ 'Text:Inside render', 'AsyncText:OutsideAsync render', + 'Text:Fallback:Inside destroy insertion', 'Text:Fallback:Inside destroy layout', + 'AsyncText:FallbackAsync destroy insertion', 'AsyncText:FallbackAsync destroy layout', + 'Text:Fallback:Outside destroy insertion', 'Text:Fallback:Outside destroy layout', + 'AsyncText:OutsideAsync create insertion', 'Text:Inside create layout', 'AsyncText:OutsideAsync create layout', 'Text:Fallback:Inside destroy passive', @@ -1682,6 +1842,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', @@ -1703,6 +1865,7 @@ describe('ReactSuspenseEffectsSemantics', () => { 'Text:Fallback render', 'Text:Outside render', 'Text:Inside destroy layout', + 'Text:Fallback create insertion', 'Text:Fallback create layout', ]); await waitForAll(['Text:Fallback create passive']); @@ -1721,6 +1884,7 @@ describe('ReactSuspenseEffectsSemantics', () => { }); assertLog([ 'Text:Inside render', + 'Text:Fallback destroy insertion', 'Text:Fallback destroy layout', 'Text:Inside create layout', 'Text:Fallback destroy passive', @@ -1756,6 +1920,12 @@ describe('ReactSuspenseEffectsSemantics', () => { function App({children = null}) { Scheduler.log('App render'); + React.useInsertionEffect(() => { + Scheduler.log('App create insertion'); + return () => { + Scheduler.log('App destroy insertion'); + }; + }, []); React.useLayoutEffect(() => { Scheduler.log('App create layout'); return () => { @@ -1787,6 +1957,9 @@ describe('ReactSuspenseEffectsSemantics', () => { 'ThrowsInDidMount render', 'Text:Inside render', 'Text:Outside render', + 'Text:Inside create insertion', + 'Text:Outside create insertion', + 'App create insertion', 'ThrowsInDidMount componentDidMount', 'Text:Inside create layout', 'Text:Outside create layout', @@ -1820,6 +1993,7 @@ describe('ReactSuspenseEffectsSemantics', () => { 'Text:Outside render', 'ThrowsInDidMount componentWillUnmount', 'Text:Inside destroy layout', + 'Text:Fallback create insertion', 'Text:Fallback create layout', 'Text:Fallback create passive', ]); @@ -1841,7 +2015,9 @@ describe('ReactSuspenseEffectsSemantics', () => { 'AsyncText:Async render', 'ThrowsInDidMount render', 'Text:Inside render', + 'Text:Fallback destroy insertion', 'Text:Fallback destroy layout', + 'AsyncText:Async create insertion', 'AsyncText:Async create layout', // Even though an error was thrown in componentDidMount, @@ -1854,10 +2030,14 @@ describe('ReactSuspenseEffectsSemantics', () => { 'AsyncText:Async create passive', // Destroy layout and passive effects in the errored tree. + 'App destroy insertion', 'App destroy layout', + 'AsyncText:Async destroy insertion', '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', @@ -1866,6 +2046,7 @@ describe('ReactSuspenseEffectsSemantics', () => { // Render fallback 'ErrorBoundary render: catch', 'Text:Error render', + 'Text:Error create insertion', 'Text:Error create layout', 'Text:Error create passive', ]); @@ -1890,6 +2071,12 @@ describe('ReactSuspenseEffectsSemantics', () => { function App({children = null}) { Scheduler.log('App render'); + React.useInsertionEffect(() => { + Scheduler.log('App create insertion'); + return () => { + Scheduler.log('App destroy insertion'); + }; + }, []); React.useLayoutEffect(() => { Scheduler.log('App create layout'); return () => { @@ -1921,6 +2108,9 @@ describe('ReactSuspenseEffectsSemantics', () => { 'ThrowsInWillUnmount render', 'Text:Inside render', 'Text:Outside render', + 'Text:Inside create insertion', + 'Text:Outside create insertion', + 'App create insertion', 'ThrowsInWillUnmount componentDidMount', 'Text:Inside create layout', 'Text:Outside create layout', @@ -1959,12 +2149,17 @@ 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 insertion', 'App destroy layout', + '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', @@ -1973,6 +2168,7 @@ describe('ReactSuspenseEffectsSemantics', () => { // Render fallback 'ErrorBoundary render: catch', 'Text:Error render', + 'Text:Error create insertion', 'Text:Error create layout', 'Text:Error create passive', ]); @@ -2031,6 +2227,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', @@ -2064,6 +2262,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', ]); @@ -2086,10 +2285,12 @@ describe('ReactSuspenseEffectsSemantics', () => { 'ThrowsInLayoutEffect render', 'Text:Inside render', + 'Text:Fallback destroy insertion', 'Text:Fallback destroy layout', // Even though an error was thrown in useLayoutEffect, // subsequent layout effects should still be created. + 'AsyncText:Async create insertion', 'AsyncText:Async create layout', 'ThrowsInLayoutEffect useLayoutEffect create', 'Text:Inside create layout', @@ -2100,8 +2301,11 @@ describe('ReactSuspenseEffectsSemantics', () => { // Destroy layout and passive effects in the errored tree. 'App destroy layout', + 'AsyncText:Async destroy insertion', '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', @@ -2110,6 +2314,7 @@ describe('ReactSuspenseEffectsSemantics', () => { // Render fallback 'ErrorBoundary render: catch', 'Text:Error render', + 'Text:Error create insertion', 'Text:Error create layout', 'Text:Error create passive', ]); @@ -2166,6 +2371,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', @@ -2204,12 +2411,16 @@ 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', + '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', @@ -2218,6 +2429,7 @@ describe('ReactSuspenseEffectsSemantics', () => { // Render fallback 'ErrorBoundary render: catch', 'Text:Error render', + 'Text:Error create insertion', 'Text:Error create layout', 'Text:Error create passive', ]); @@ -2263,6 +2475,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', @@ -2326,6 +2539,8 @@ describe('ReactSuspenseEffectsSemantics', () => { 'AsyncText:Async_2 render', 'ClassText:Class render', 'ClassText:Fallback componentWillUnmount', + 'AsyncText:Async_1 create insertion', + 'AsyncText:Async_2 create insertion', 'Text:Function create layout', 'AsyncText:Async_1 create layout', 'AsyncText:Async_2 create layout', @@ -2346,8 +2561,11 @@ describe('ReactSuspenseEffectsSemantics', () => { ReactNoop.render(null); }); assertLog([ + 'Text:Function destroy insertion', 'Text:Function destroy layout', + 'AsyncText:Async_1 destroy insertion', 'AsyncText:Async_1 destroy layout', + 'AsyncText:Async_2 destroy insertion', 'AsyncText:Async_2 destroy layout', 'ClassText:Class componentWillUnmount', 'Text:Function destroy passive', @@ -2405,6 +2623,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', @@ -2479,6 +2698,7 @@ describe('ReactSuspenseEffectsSemantics', () => { ReactNoop.render(null); }); assertLog([ + 'Text:Function destroy insertion', 'Text:Function destroy layout', 'ClassText:Class componentWillUnmount', 'Text:Function destroy passive', @@ -2486,6 +2706,98 @@ describe('ReactSuspenseEffectsSemantics', () => { }); }); + describe('insertion effects within a tree that re-suspends in an update', () => { + // @gate enableLegacyCache + it('should be destroyed in the deleted tree', async () => { + function App({children = null}) { + Scheduler.log('App render'); + React.useInsertionEffect(() => { + Scheduler.log('App create insertion'); + return () => { + Scheduler.log('App destroy insertion'); + }; + }, []); + React.useLayoutEffect(() => { + Scheduler.log('App create layout'); + return () => { + Scheduler.log('App destroy layout'); + }; + }, []); + React.useEffect(() => { + Scheduler.log('App create passive'); + return () => { + Scheduler.log('App destroy passive'); + }; + }, []); + return ( + <> + }>{children} + + ); + } + + await act(() => { + ReactNoop.render( + + + , + ); + }); + assertLog([ + 'App render', + 'Text:One render', + 'Text:One create insertion', + 'App create insertion', + 'Text:One create layout', + 'App create layout', + 'Text:One create passive', + 'App create passive', + ]); + expect(ReactNoop).toMatchRenderedOutput(); + + // Schedule an update that causes React to suspend. + await act(async () => { + ReactNoop.render( + + + , + ); + await waitFor([ + 'App render', + 'Suspend:Two', + 'Text:Fallback render', + 'Text:One destroy layout', + 'Text:Fallback create insertion', + 'Text:Fallback create layout', + ]); + await waitForAll(['Text:Fallback create passive']); + expect(ReactNoop).toMatchRenderedOutput( + <> +