[DevTools] Unmount fallbacks in the context of the parent Suspense (#34475)

Co-authored-by: Ruslan Lesiutin <hoxy@meta.com>
This commit is contained in:
Sebastian "Sebbie" Silbermann
2025-09-13 11:03:32 +02:00
committed by GitHub
parent 8a8e9a7edf
commit 5502d85cc7
2 changed files with 60 additions and 2 deletions

View File

@@ -3079,6 +3079,10 @@ describe('Store', () => {
<Suspense name="head-fallback" rects={[{x:1,y:2,width:10,height:1}]}>
<Suspense name="main" rects={[{x:1,y:2,width:4,height:1}]}>
`);
await actAsync(() => render(null));
expect(store).toMatchInlineSnapshot(``);
});
it('should handle an empty root', async () => {

View File

@@ -3070,6 +3070,24 @@ export function attach(
}
}
function unmountSuspenseChildrenRecursively(
contentInstance: DevToolsInstance,
stashedSuspenseParent: null | SuspenseNode,
stashedSuspensePrevious: null | SuspenseNode,
stashedSuspenseRemaining: null | SuspenseNode,
): void {
// First unmount only the Offscreen boundary. I.e. the main content.
unmountInstanceRecursively(contentInstance);
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
// unmount the fallback, unmounting anything in the context of the parent SuspenseNode.
// Since the fallback conceptually blocks the parent.
reconcilingParentSuspenseNode = stashedSuspenseParent;
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
unmountRemainingChildren();
}
function isChildOf(
parentInstance: DevToolsInstance,
childInstance: DevToolsInstance,
@@ -4015,6 +4033,7 @@ export function attach(
debug('unmountInstanceRecursively()', instance, reconcilingParent);
}
let shouldPopSuspenseNode = false;
const stashedParent = reconcilingParent;
const stashedPrevious = previouslyReconciledSibling;
const stashedRemaining = remainingReconcilingChildren;
@@ -4035,11 +4054,46 @@ export function attach(
previouslyReconciledSiblingSuspenseNode = null;
remainingReconcilingChildrenSuspenseNodes =
instance.suspenseNode.firstChild;
shouldPopSuspenseNode = true;
}
try {
// Unmount the remaining set.
unmountRemainingChildren();
if (
(instance.kind === FIBER_INSTANCE ||
instance.kind === FILTERED_FIBER_INSTANCE) &&
instance.data.tag === SuspenseComponent &&
OffscreenComponent !== -1
) {
const fiber = instance.data;
const contentFiberInstance = remainingReconcilingChildren;
const hydrated = isFiberHydrated(fiber);
if (hydrated) {
if (contentFiberInstance === null) {
throw new Error(
'There should always be an Offscreen Fiber child in a hydrated Suspense boundary.',
);
}
unmountSuspenseChildrenRecursively(
contentFiberInstance,
stashedSuspenseParent,
stashedSuspensePrevious,
stashedSuspenseRemaining,
);
// unmountSuspenseChildren already popped
shouldPopSuspenseNode = false;
} else {
if (contentFiberInstance !== null) {
throw new Error(
'A dehydrated Suspense node should not have a content Fiber.',
);
}
}
} else {
unmountRemainingChildren();
}
removePreviousSuspendedBy(
instance,
previousSuspendedBy,
@@ -4049,7 +4103,7 @@ export function attach(
reconcilingParent = stashedParent;
previouslyReconciledSibling = stashedPrevious;
remainingReconcilingChildren = stashedRemaining;
if (instance.suspenseNode !== null) {
if (shouldPopSuspenseNode) {
reconcilingParentSuspenseNode = stashedSuspenseParent;
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;