Skip to content

Commit 8dba931

Browse files
authored
[DevTools] Handle fallback unmount in Suspense update path (#34199)
1 parent 2d98b45 commit 8dba931

File tree

2 files changed

+94
-10
lines changed

2 files changed

+94
-10
lines changed

packages/react-devtools-shared/src/__tests__/store-test.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2696,4 +2696,83 @@ describe('Store', () => {
26962696
<ClientComponent key="D">
26972697
`);
26982698
});
2699+
2700+
// @reactVersion >= 18.0
2701+
it('can reconcile Suspense in fallback positions', async () => {
2702+
let resolveFallback;
2703+
const fallbackPromise = new Promise(resolve => {
2704+
resolveFallback = resolve;
2705+
});
2706+
let resolveContent;
2707+
const contentPromise = new Promise(resolve => {
2708+
resolveContent = resolve;
2709+
});
2710+
2711+
function Component({children, promise}) {
2712+
if (promise) {
2713+
React.use(promise);
2714+
}
2715+
return <div>{children}</div>;
2716+
}
2717+
2718+
await actAsync(() =>
2719+
render(
2720+
<React.Suspense
2721+
name="content"
2722+
fallback={
2723+
<React.Suspense
2724+
name="fallback"
2725+
fallback={
2726+
<Component key="fallback-fallback">
2727+
Loading fallback...
2728+
</Component>
2729+
}>
2730+
<Component key="fallback-content" promise={fallbackPromise}>
2731+
Loading...
2732+
</Component>
2733+
</React.Suspense>
2734+
}>
2735+
<Component key="content" promise={contentPromise}>
2736+
done
2737+
</Component>
2738+
</React.Suspense>,
2739+
),
2740+
);
2741+
2742+
expect(store).toMatchInlineSnapshot(`
2743+
[root]
2744+
▾ <Suspense name="content">
2745+
▾ <Suspense name="fallback">
2746+
<Component key="fallback-fallback">
2747+
[shell]
2748+
<Suspense name="content" rects={null}>
2749+
<Suspense name="fallback" rects={null}>
2750+
`);
2751+
2752+
await actAsync(() => {
2753+
resolveFallback();
2754+
});
2755+
2756+
expect(store).toMatchInlineSnapshot(`
2757+
[root]
2758+
▾ <Suspense name="content">
2759+
▾ <Suspense name="fallback">
2760+
<Component key="fallback-content">
2761+
[shell]
2762+
<Suspense name="content" rects={null}>
2763+
<Suspense name="fallback" rects={[{x:1,y:2,width:10,height:1}]}>
2764+
`);
2765+
2766+
await actAsync(() => {
2767+
resolveContent();
2768+
});
2769+
2770+
expect(store).toMatchInlineSnapshot(`
2771+
[root]
2772+
▾ <Suspense name="content">
2773+
<Component key="content">
2774+
[shell]
2775+
<Suspense name="content" rects={[{x:1,y:2,width:4,height:1}]}>
2776+
`);
2777+
});
26992778
});

packages/react-devtools-shared/src/backend/fiber/renderer.js

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4736,34 +4736,39 @@ export function attach(
47364736
);
47374737
47384738
shouldMeasureSuspenseNode = false;
4739-
if (nextFallbackFiber !== null) {
4739+
if (prevFallbackFiber !== null || nextFallbackFiber !== null) {
47404740
const fallbackStashedSuspenseParent = reconcilingParentSuspenseNode;
47414741
const fallbackStashedSuspensePrevious =
47424742
previouslyReconciledSiblingSuspenseNode;
47434743
const fallbackStashedSuspenseRemaining =
47444744
remainingReconcilingChildrenSuspenseNodes;
47454745
// Next, we'll pop back out of the SuspenseNode that we added above and now we'll
4746-
// reconcile the fallback, reconciling anything by inserting into the parent SuspenseNode.
4746+
// reconcile the fallback, reconciling anything in the context of the parent SuspenseNode.
47474747
// Since the fallback conceptually blocks the parent.
47484748
reconcilingParentSuspenseNode = stashedSuspenseParent;
47494749
previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
47504750
remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
47514751
try {
4752-
updateFlags |= updateVirtualChildrenRecursively(
4753-
nextFallbackFiber,
4754-
null,
4755-
prevFallbackFiber,
4756-
traceNearestHostComponentUpdate,
4757-
0,
4758-
);
4752+
if (nextFallbackFiber === null) {
4753+
unmountRemainingChildren();
4754+
} else {
4755+
updateFlags |= updateVirtualChildrenRecursively(
4756+
nextFallbackFiber,
4757+
null,
4758+
prevFallbackFiber,
4759+
traceNearestHostComponentUpdate,
4760+
0,
4761+
);
4762+
}
47594763
} finally {
47604764
reconcilingParentSuspenseNode = fallbackStashedSuspenseParent;
47614765
previouslyReconciledSiblingSuspenseNode =
47624766
fallbackStashedSuspensePrevious;
47634767
remainingReconcilingChildrenSuspenseNodes =
47644768
fallbackStashedSuspenseRemaining;
47654769
}
4766-
} else if (nextFiber.memoizedState === null) {
4770+
}
4771+
if (nextFiber.memoizedState === null) {
47674772
// Measure this Suspense node in case it changed. We don't update the rect while
47684773
// we're inside a disconnected subtree nor if we are the Suspense boundary that
47694774
// is suspended. This lets us keep the rectangle of the displayed content while

0 commit comments

Comments
 (0)