diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js index 56795d232895c..a6dc02f844e5f 100644 --- a/packages/react-devtools-shared/src/__tests__/store-test.js +++ b/packages/react-devtools-shared/src/__tests__/store-test.js @@ -13,6 +13,7 @@ describe('Store', () => { let ReactDOMClient; let agent; let act; + let actAsync; let bridge; let getRendererID; let legacyRender; @@ -30,6 +31,7 @@ describe('Store', () => { const utils = require('./utils'); act = utils.act; + actAsync = utils.actAsync; getRendererID = utils.getRendererID; legacyRender = utils.legacyRender; withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored; @@ -2064,5 +2066,47 @@ describe('Store', () => { expect(store.errorCount).toBe(0); expect(store.warningCount).toBe(0); }); + + // Regression test for https://github.com/facebook/react/issues/23202 + // @reactVersion >= 18.0 + it('suspense boundary children should not double unmount and error', async () => { + async function fakeImport(result) { + return {default: result}; + } + + const ChildA = () => null; + const ChildB = () => null; + + const LazyChildA = React.lazy(() => fakeImport(ChildA)); + const LazyChildB = React.lazy(() => fakeImport(ChildB)); + + function App({renderA}) { + return ( + + {renderA ? : } + + ); + } + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await actAsync(() => root.render()); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾ + + `); + + await actAsync(() => root.render()); + + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + ▾ + + `); + }); }); }); diff --git a/packages/react-devtools-shared/src/backend/renderer.js b/packages/react-devtools-shared/src/backend/renderer.js index 9959cad7aa525..f2d6dad45f002 100644 --- a/packages/react-devtools-shared/src/backend/renderer.js +++ b/packages/react-devtools-shared/src/backend/renderer.js @@ -2632,14 +2632,15 @@ export function attach( } function handleCommitFiberUnmount(fiber) { - // Flush any pending Fibers that we are untracking before processing the new commit. - // If we don't do this, we might end up double-deleting Fibers in some cases (like Legacy Suspense). - untrackFibers(); - - // This is not recursive. - // We can't traverse fibers after unmounting so instead - // we rely on React telling us about each unmount. - recordUnmount(fiber, false); + // If the untrackFiberSet already has the unmounted Fiber, this means we've already + // recordedUnmount, so we don't need to do it again. If we don't do this, we might + // end up double-deleting Fibers in some cases (like Legacy Suspense). + if (!untrackFibersSet.has(fiber)) { + // This is not recursive. + // We can't traverse fibers after unmounting so instead + // we rely on React telling us about each unmount. + recordUnmount(fiber, false); + } } function handlePostCommitFiberRoot(root) {