[DevTools] Only block child Suspense boundaries if the parent has all shared suspenders removed (#35737)

This commit is contained in:
Sebastian "Sebbie" Silbermann
2026-02-10 17:52:35 +01:00
committed by GitHub
parent 70890e7c58
commit 57b79b0388
2 changed files with 107 additions and 9 deletions

View File

@@ -3617,6 +3617,103 @@ describe('Store', () => {
`);
});
// @reactVersion >= 17.0
it('continues to consider Suspense boundary as blocking if some child still is suspended on removed io', async () => {
function Component({promise}) {
readValue(promise);
return null;
}
let resolve;
const promise = new Promise(_resolve => {
resolve = _resolve;
});
await actAsync(() => {
render(
<React.Suspense fallback={null} name="outer">
<Component key="one" promise={promise} />
<Component key="two" promise={promise} />
<React.Suspense fallback={null} name="inner">
<Component key="three" promise={promise} />
</React.Suspense>
</React.Suspense>,
);
});
expect(store).toMatchInlineSnapshot(`
[root]
<Suspense name="outer">
[suspense-root] rects={null}
<Suspense name="outer" uniqueSuspenders={true} rects={null}>
`);
await actAsync(() => {
resolve('Hello, World!');
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Suspense name="outer">
<Component key="one">
<Component key="two">
▾ <Suspense name="inner">
<Component key="three">
[suspense-root] rects={null}
<Suspense name="outer" uniqueSuspenders={true} rects={null}>
<Suspense name="inner" uniqueSuspenders={false} rects={null}>
`);
// We remove one suspender.
// The inner one shouldn't have unique suspenders because it's still blocked
// by the outer one.
await actAsync(() => {
render(
<React.Suspense fallback={null} name="outer">
<Component key="one" promise={promise} />
<React.Suspense fallback={null} name="inner">
<Component key="three" promise={promise} />
</React.Suspense>
</React.Suspense>,
);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Suspense name="outer">
<Component key="one">
▾ <Suspense name="inner">
<Component key="three">
[suspense-root] rects={null}
<Suspense name="outer" uniqueSuspenders={true} rects={null}>
<Suspense name="inner" uniqueSuspenders={false} rects={null}>
`);
// Now we remove all unique suspenders of the outer Suspense boundary.
// The inner one is now independently revealed from the parent and should
// be marked as having unique suspenders.
// TODO: The outer boundary no longer has unique suspenders.
await actAsync(() => {
render(
<React.Suspense fallback={null} name="outer">
<React.Suspense fallback={null} name="inner">
<Component key="three" promise={promise} />
</React.Suspense>
</React.Suspense>,
);
});
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Suspense name="outer">
▾ <Suspense name="inner">
<Component key="three">
[suspense-root] rects={null}
<Suspense name="outer" uniqueSuspenders={true} rects={null}>
<Suspense name="inner" uniqueSuspenders={true} rects={null}>
`);
});
// @reactVersion >= 19
it('cleans up host hoistables', async () => {
function Left() {

View File

@@ -3197,15 +3197,16 @@ export function attach(
environmentCounts.set(env, count - 1);
}
}
}
if (
suspenseNode.hasUniqueSuspenders &&
!ioExistsInSuspenseAncestor(suspenseNode, ioInfo)
) {
// This entry wasn't in any ancestor and is no longer in this suspense boundary.
// This means that a child might now be the unique suspender for this IO.
// Search the child boundaries to see if we can reveal any of them.
unblockSuspendedBy(suspenseNode, ioInfo);
if (
suspenseNode.hasUniqueSuspenders &&
!ioExistsInSuspenseAncestor(suspenseNode, ioInfo)
) {
// This entry wasn't in any ancestor and is no longer in this suspense boundary.
// This means that a child might now be the unique suspender for this IO.
// Search the child boundaries to see if we can reveal any of them.
unblockSuspendedBy(suspenseNode, ioInfo);
}
}
}
}