[DevTools] Handle fallback unmount in Suspense update path (#34199)

This commit is contained in:
Sebastian "Sebbie" Silbermann
2025-08-15 19:40:35 +02:00
committed by GitHub
parent 2d98b45d92
commit 8dba9311e5
2 changed files with 94 additions and 10 deletions
@@ -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}]}>
`);
});
});
+15 -10
View File
@@ -4736,26 +4736,30 @@ 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 =
@@ -4763,7 +4767,8 @@ export function attach(
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