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
79 changes: 79 additions & 0 deletions packages/react-devtools-shared/src/__tests__/store-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2696,4 +2696,83 @@ describe('Store', () => {
<ClientComponent key="D">
`);
});

// @reactVersion >= 18.0
it('can reconcile Suspense in fallback positions', async () => {
let resolveFallback;
const fallbackPromise = new Promise(resolve => {
resolveFallback = resolve;
});
let resolveContent;
const contentPromise = new Promise(resolve => {
resolveContent = resolve;
});

function Component({children, promise}) {
if (promise) {
React.use(promise);
}
return <div>{children}</div>;
}

await actAsync(() =>
render(
<React.Suspense
name="content"
fallback={
<React.Suspense
name="fallback"
fallback={
<Component key="fallback-fallback">
Loading fallback...
</Component>
}>
<Component key="fallback-content" promise={fallbackPromise}>
Loading...
</Component>
</React.Suspense>
}>
<Component key="content" promise={contentPromise}>
done
</Component>
</React.Suspense>,
),
);

expect(store).toMatchInlineSnapshot(`
[root]
▾ <Suspense name="content">
▾ <Suspense name="fallback">
<Component key="fallback-fallback">
[shell]
<Suspense name="content" rects={null}>
<Suspense name="fallback" rects={null}>
`);

await actAsync(() => {
resolveFallback();
});

expect(store).toMatchInlineSnapshot(`
[root]
▾ <Suspense name="content">
▾ <Suspense name="fallback">
<Component key="fallback-content">
[shell]
<Suspense name="content" rects={null}>
<Suspense name="fallback" rects={[{x:1,y:2,width:10,height:1}]}>
`);

await actAsync(() => {
resolveContent();
});

expect(store).toMatchInlineSnapshot(`
[root]
▾ <Suspense name="content">
<Component key="content">
[shell]
<Suspense name="content" rects={[{x:1,y:2,width:4,height:1}]}>
`);
});
});
25 changes: 15 additions & 10 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4736,34 +4736,39 @@ export function attach(
);

shouldMeasureSuspenseNode = false;
if (nextFallbackFiber !== null) {
if (prevFallbackFiber !== null || nextFallbackFiber !== null) {
const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
const fallbackStashedSuspensePrevious =
previouslyReconciledSiblingSuspenseNode;
const fallbackStashedSuspenseRemaining =
remainingReconcilingChildrenSuspenseNodes;
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
// reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
// reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
// Since the fallback conceptually blocks the parent.
reconcilingParentSuspenseNode = stashedSuspenseParent;
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
try {
updateFlags |= updateVirtualChildrenRecursively(
nextFallbackFiber,
null,
prevFallbackFiber,
traceNearestHostComponentUpdate,
0,
);
if (nextFallbackFiber === null) {
unmountRemainingChildren();
} else {
updateFlags |= updateVirtualChildrenRecursively(
nextFallbackFiber,
null,
prevFallbackFiber,
traceNearestHostComponentUpdate,
0,
);
}
} finally {
reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
previouslyReconciledSiblingSuspenseNode =
fallbackStashedSuspensePrevious;
remainingReconcilingChildrenSuspenseNodes =
fallbackStashedSuspenseRemaining;
}
} else if (nextFiber.memoizedState === null) {
}
if (nextFiber.memoizedState === null) {
// Measure this Suspense node in case it changed. We don't update the rect while
// we're inside a disconnected subtree nor if we are the Suspense boundary that
// is suspended. This lets us keep the rectangle of the displayed content while
Expand Down
Loading