Skip to content

Commit 73507ec

Browse files
authored
[DevTools] Exclude Suspense boundaries in hidden Activity (#34756)
1 parent 03a62b2 commit 73507ec

File tree

2 files changed

+138
-7
lines changed

2 files changed

+138
-7
lines changed

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

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3142,4 +3142,105 @@ describe('Store', () => {
31423142
await actAsync(() => render(null));
31433143
expect(store).toMatchInlineSnapshot(``);
31443144
});
3145+
3146+
// @reactVersion >= 19
3147+
it('should keep suspended boundaries in the Suspense tree but not hidden Activity', async () => {
3148+
const Activity = React.Activity || React.unstable_Activity;
3149+
3150+
const never = new Promise(() => {});
3151+
function Never() {
3152+
readValue(never);
3153+
return null;
3154+
}
3155+
function Component({children}) {
3156+
return <div>{children}</div>;
3157+
}
3158+
3159+
function App({hidden}) {
3160+
return (
3161+
<>
3162+
<Activity mode={hidden ? 'hidden' : 'visible'}>
3163+
<React.Suspense name="inside-activity">
3164+
<Component key="inside-activity">inside Activity</Component>
3165+
</React.Suspense>
3166+
</Activity>
3167+
<React.Suspense name="outer-suspense">
3168+
<React.Suspense name="inner-suspense">
3169+
<Component key="inside-suspense">inside Suspense</Component>
3170+
</React.Suspense>
3171+
{hidden ? <Never /> : null}
3172+
</React.Suspense>
3173+
</>
3174+
);
3175+
}
3176+
3177+
await actAsync(() => {
3178+
render(<App hidden={true} />);
3179+
});
3180+
3181+
expect(store).toMatchInlineSnapshot(`
3182+
[root]
3183+
▾ <App>
3184+
<Activity>
3185+
<Suspense name="outer-suspense">
3186+
[suspense-root] rects={[{x:1,y:2,width:15,height:1}]}
3187+
<Suspense name="outer-suspense" rects={null}>
3188+
`);
3189+
3190+
// mount as visible
3191+
await actAsync(() => {
3192+
render(null);
3193+
});
3194+
await actAsync(() => {
3195+
render(<App hidden={false} />);
3196+
});
3197+
3198+
expect(store).toMatchInlineSnapshot(`
3199+
[root]
3200+
▾ <App>
3201+
▾ <Activity>
3202+
▾ <Suspense name="inside-activity">
3203+
<Component key="inside-activity">
3204+
▾ <Suspense name="outer-suspense">
3205+
▾ <Suspense name="inner-suspense">
3206+
<Component key="inside-suspense">
3207+
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
3208+
<Suspense name="inside-activity" rects={[{x:1,y:2,width:15,height:1}]}>
3209+
<Suspense name="outer-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
3210+
<Suspense name="inner-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
3211+
`);
3212+
3213+
await actAsync(() => {
3214+
render(<App hidden={true} />);
3215+
});
3216+
3217+
expect(store).toMatchInlineSnapshot(`
3218+
[root]
3219+
▾ <App>
3220+
<Activity>
3221+
<Suspense name="outer-suspense">
3222+
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
3223+
<Suspense name="outer-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
3224+
<Suspense name="inner-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
3225+
`);
3226+
3227+
await actAsync(() => {
3228+
render(<App hidden={false} />);
3229+
});
3230+
3231+
expect(store).toMatchInlineSnapshot(`
3232+
[root]
3233+
▾ <App>
3234+
▾ <Activity>
3235+
▾ <Suspense name="inside-activity">
3236+
<Component key="inside-activity">
3237+
▾ <Suspense name="outer-suspense">
3238+
▾ <Suspense name="inner-suspense">
3239+
<Component key="inside-suspense">
3240+
[suspense-root] rects={[{x:1,y:2,width:15,height:1}, {x:1,y:2,width:15,height:1}]}
3241+
<Suspense name="inside-activity" rects={[{x:1,y:2,width:15,height:1}]}>
3242+
<Suspense name="outer-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
3243+
<Suspense name="inner-suspense" rects={[{x:1,y:2,width:15,height:1}]}>
3244+
`);
3245+
});
31453246
});

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

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3144,12 +3144,30 @@ export function attach(
31443144
}
31453145
}
31463146
3147+
/**
3148+
* Offscreen of suspended Suspense
3149+
*/
3150+
function isSuspendedOffscreen(fiber: Fiber): boolean {
3151+
switch (fiber.tag) {
3152+
case LegacyHiddenComponent:
3153+
// fallthrough since all published implementations currently implement the same state as Offscreen.
3154+
case OffscreenComponent:
3155+
return (
3156+
fiber.memoizedState !== null &&
3157+
fiber.return !== null &&
3158+
fiber.return.tag === SuspenseComponent
3159+
);
3160+
default:
3161+
return false;
3162+
}
3163+
}
3164+
31473165
function unmountRemainingChildren() {
31483166
if (
31493167
reconcilingParent !== null &&
31503168
(reconcilingParent.kind === FIBER_INSTANCE ||
31513169
reconcilingParent.kind === FILTERED_FIBER_INSTANCE) &&
3152-
isHiddenOffscreen(reconcilingParent.data) &&
3170+
isSuspendedOffscreen(reconcilingParent.data) &&
31533171
!isInDisconnectedSubtree
31543172
) {
31553173
// This is a hidden offscreen, we need to execute this in the context of a disconnected subtree.
@@ -4026,7 +4044,7 @@ export function attach(
40264044
trackDebugInfoFromHostComponent(nearestInstance, fiber);
40274045
}
40284046
4029-
if (isHiddenOffscreen(fiber)) {
4047+
if (isSuspendedOffscreen(fiber)) {
40304048
// If an Offscreen component is hidden, mount its children as disconnected.
40314049
const stashedDisconnected = isInDisconnectedSubtree;
40324050
isInDisconnectedSubtree = true;
@@ -4037,6 +4055,9 @@ export function attach(
40374055
} finally {
40384056
isInDisconnectedSubtree = stashedDisconnected;
40394057
}
4058+
} else if (isHiddenOffscreen(fiber)) {
4059+
// hidden Activity is noisy.
4060+
// Including it may show overlapping Suspense rects
40404061
} else if (fiber.tag === SuspenseComponent && OffscreenComponent === -1) {
40414062
// Legacy Suspense without the Offscreen wrapper. For the modern Suspense we just handle the
40424063
// Offscreen wrapper itself specially.
@@ -4981,6 +5002,8 @@ export function attach(
49815002
49825003
const prevWasHidden = isHiddenOffscreen(prevFiber);
49835004
const nextIsHidden = isHiddenOffscreen(nextFiber);
5005+
const prevWasSuspended = isSuspendedOffscreen(prevFiber);
5006+
const nextIsSuspended = isSuspendedOffscreen(nextFiber);
49845007
49855008
if (isLegacySuspense) {
49865009
if (
@@ -5058,8 +5081,8 @@ export function attach(
50585081
);
50595082
updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
50605083
}
5061-
} else if (nextIsHidden) {
5062-
if (!prevWasHidden) {
5084+
} else if (nextIsSuspended) {
5085+
if (!prevWasSuspended) {
50635086
// We're hiding the children. Disconnect them from the front end but keep state.
50645087
if (fiberInstance !== null && !isInDisconnectedSubtree) {
50655088
disconnectChildrenRecursively(remainingReconcilingChildren);
@@ -5077,7 +5100,7 @@ export function attach(
50775100
} finally {
50785101
isInDisconnectedSubtree = stashedDisconnected;
50795102
}
5080-
} else if (prevWasHidden && !nextIsHidden) {
5103+
} else if (prevWasSuspended && !nextIsSuspended) {
50815104
// We're revealing the hidden children. We now need to update them to the latest state.
50825105
// We do this while still in the disconnected state and then we reconnect the new ones.
50835106
// This avoids reconnecting things that are about to be removed anyway.
@@ -5103,6 +5126,13 @@ export function attach(
51035126
// Children may have reordered while they were hidden.
51045127
updateFlags |= ShouldResetChildren | ShouldResetSuspenseChildren;
51055128
}
5129+
} else if (nextIsHidden) {
5130+
if (prevWasHidden) {
5131+
// still hidden. Nothing to do.
5132+
} else {
5133+
// We're hiding the children. Remove them from the Frontend
5134+
unmountRemainingChildren();
5135+
}
51065136
} else if (
51075137
nextFiber.tag === SuspenseComponent &&
51085138
OffscreenComponent !== -1 &&
@@ -5259,7 +5289,7 @@ export function attach(
52595289
// We need to crawl the subtree for closest non-filtered Fibers
52605290
// so that we can display them in a flat children set.
52615291
if (fiberInstance !== null && fiberInstance.kind === FIBER_INSTANCE) {
5262-
if (!nextIsHidden && !isInDisconnectedSubtree) {
5292+
if (!nextIsSuspended && !isInDisconnectedSubtree) {
52635293
recordResetChildren(fiberInstance);
52645294
}
52655295
@@ -5335,7 +5365,7 @@ export function attach(
53355365
if (
53365366
(child.kind === FIBER_INSTANCE ||
53375367
child.kind === FILTERED_FIBER_INSTANCE) &&
5338-
isHiddenOffscreen(child.data)
5368+
isSuspendedOffscreen(child.data)
53395369
) {
53405370
// This instance's children are already disconnected.
53415371
} else {

0 commit comments

Comments
 (0)