diff --git a/packages/react-devtools-shared/src/__tests__/store-test.js b/packages/react-devtools-shared/src/__tests__/store-test.js index 37272ea0579..c4fe7d337dd 100644 --- a/packages/react-devtools-shared/src/__tests__/store-test.js +++ b/packages/react-devtools-shared/src/__tests__/store-test.js @@ -3143,6 +3143,59 @@ describe('Store', () => { expect(store).toMatchInlineSnapshot(``); }); + // @reactVersion >= 17.0 + it('should track suspended by in filtered fallback', async () => { + function IgnoreMe({promise}) { + return readValue(promise); + } + + function Component({promise}) { + return readValue(promise); + } + + await actAsync( + async () => + (store.componentFilters = [createDisplayNameFilter('^IgnoreMe', true)]), + ); + + let resolveFallback; + const fallbackPromise = new Promise(resolve => { + resolveFallback = resolve; + }); + let resolveContent; + const contentPromise = new Promise(resolve => { + resolveContent = resolve; + }); + + await actAsync(() => + render( + }> + + , + ), + ); + expect(store).toMatchInlineSnapshot(``); + + await actAsync(() => resolveFallback('loading')); + expect(store).toMatchInlineSnapshot(` + [root] + + [suspense-root] rects={null} + + `); + + await actAsync(() => resolveContent('content')); + expect(store).toMatchInlineSnapshot(` + [root] + ▾ + + [suspense-root] rects={null} + + `); + }); + // @reactVersion >= 19 it('should keep suspended boundaries in the Suspense tree but not hidden Activity', async () => { const Activity = React.Activity || React.unstable_Activity; diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 4f755fda293..6848e327f67 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -2992,6 +2992,30 @@ export function attach( ) { parentInstance = parentInstance.parent; } + if (parentInstance.kind === FIBER_INSTANCE) { + const fiber = parentInstance.data; + + if ( + fiber.tag === SuspenseComponent && + parentInstance !== parentSuspenseNode.instance + ) { + // We're about to attach async info to a Suspense boundary we're not + // actually considering the parent Suspense boundary for this async info. + // We must have not found a suitable Fiber inside the fallback (e.g. due to filtering). + // Use the parent of this instance instead since we treat async info + // attached to a Suspense boundary as that async info triggering the + // fallback of that boundary. + const parent = parentInstance.parent; + if (parent === null) { + // This shouldn't happen. Any would have at least have the + // host root as the parent which can't have a fallback. + throw new Error( + 'Did not find a suitable instance for this async info. This is a bug in React.', + ); + } + parentInstance = parent; + } + } const suspenseNodeSuspendedBy = parentSuspenseNode.suspendedBy; const ioInfo = asyncInfo.awaited; @@ -5255,9 +5279,9 @@ export function attach( // It might even result in a bad user experience for e.g. node selection in the Elements panel. // The easiest fix is to strip out the intermediate Fragment fibers, // so the Elements panel and Profiler don't need to special case them. - // Suspense components only have a non-null memoizedState if they're timed-out. const isLegacySuspense = nextFiber.tag === SuspenseComponent && OffscreenComponent === -1; + // Suspense components only have a non-null memoizedState if they're timed-out. const prevDidTimeout = isLegacySuspense && prevFiber.memoizedState !== null; const nextDidTimeOut =