From 4f3f7eeb7f467be7a8c5e7c2cdf64cb95a6099c3 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 4 Sep 2020 17:46:44 -0500 Subject: [PATCH] Bugfix: Effect clean up when deleting suspended tree (#19752) * Bug: Effect clean up when deleting suspended tree Adds a failing unit test. * Re-use static flags from suspended primary tree When switching to a Suspense boundary's fallback, we need to be sure to preserve static subtree flags from the primary tree. --- .../src/ReactFiberBeginWork.new.js | 7 ++ .../ReactSuspenseWithNoopRenderer-test.js | 97 +++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index 2d3841d2e0e43..fa27d3dcad74e 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -64,6 +64,7 @@ import { Ref, Deletion, ForceUpdateForLegacySuspense, + StaticMask, } from './ReactFiberFlags'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import { @@ -2131,6 +2132,12 @@ function updateSuspenseFallbackChildren( currentPrimaryChildFragment, primaryChildProps, ); + + // Since we're reusing a current tree, we need to reuse the flags, too. + // (We don't do this in legacy mode, because in legacy mode we don't re-use + // the current tree; see previous branch.) + primaryChildFragment.subtreeFlags = + currentPrimaryChildFragment.subtreeFlags & StaticMask; } let fallbackChildFragment; if (currentFallbackChildFragment !== null) { diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js index c10772eaa16e5..b6ed0d2be4d40 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js @@ -3956,4 +3956,101 @@ describe('ReactSuspenseWithNoopRenderer', () => { , ); }); + + it('should fire effect clean-up when deleting suspended tree', async () => { + const {useEffect} = React; + + function App({show}) { + return ( + }> + + {show && } + + ); + } + + function Child() { + useEffect(() => { + Scheduler.unstable_yieldValue('Mount Child'); + return () => { + Scheduler.unstable_yieldValue('Unmount Child'); + }; + }, []); + return ; + } + + const root = ReactNoop.createRoot(); + + await ReactNoop.act(async () => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Mount Child']); + expect(root).toMatchRenderedOutput(); + + await ReactNoop.act(async () => { + root.render(); + }); + // TODO: `act` should have already flushed the placeholder, so this + // runAllTimers call should be unnecessary. + jest.runAllTimers(); + expect(Scheduler).toHaveYielded(['Suspend! [Async]', 'Loading...']); + expect(root).toMatchRenderedOutput( + <> +