Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions packages/react-devtools-shared/src/__tests__/store-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<React.Suspense
name="main"
fallback={<IgnoreMe promise={fallbackPromise} />}>
<Component promise={contentPromise} />
</React.Suspense>,
),
);
expect(store).toMatchInlineSnapshot(``);

await actAsync(() => resolveFallback('loading'));
expect(store).toMatchInlineSnapshot(`
[root]
<Suspense name="main">
[suspense-root] rects={null}
<Suspense name="main" rects={null}>
`);

await actAsync(() => resolveContent('content'));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Suspense name="main">
<Component>
[suspense-root] rects={null}
<Suspense name="main" 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;
Expand Down
26 changes: 25 additions & 1 deletion packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Suspense> 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;
Expand Down Expand Up @@ -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 =
Expand Down
Loading