@samitouri / QOS-React-1 / commits / 57b79b0388

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

Sebastian "Sebbie" Silbermann committed Feb 10, 2026 at 17:52 UTC 57b79b0388b755096216b2b5308113e54eac3be8
2 files changed +107 -9
packages/react-devtools-shared/src/__tests__/store-test.js
+97
@@ -3617,6 +3617,103 @@ describe('Store', () => {
3617 `);
3618 });
3619
3620 + // @reactVersion >= 17.0
3621 + it('continues to consider Suspense boundary as blocking if some child still is suspended on removed io', async () => {
3622 + function Component({promise}) {
3623 + readValue(promise);
3624 + return null;
3625 + }
3626 +
3627 + let resolve;
3628 + const promise = new Promise(_resolve => {
3629 + resolve = _resolve;
3630 + });
3631 +
3632 + await actAsync(() => {
3633 + render(
3634 + <React.Suspense fallback={null} name="outer">
3635 + <Component key="one" promise={promise} />
3636 + <Component key="two" promise={promise} />
3637 + <React.Suspense fallback={null} name="inner">
3638 + <Component key="three" promise={promise} />
3639 + </React.Suspense>
3640 + </React.Suspense>,
3641 + );
3642 + });
3643 +
3644 + expect(store).toMatchInlineSnapshot(`
3645 + [root]
3646 + <Suspense name="outer">
3647 + [suspense-root] rects={null}
3648 + <Suspense name="outer" uniqueSuspenders={true} rects={null}>
3649 + `);
3650 +
3651 + await actAsync(() => {
3652 + resolve('Hello, World!');
3653 + });
3654 +
3655 + expect(store).toMatchInlineSnapshot(`
3656 + [root]
3657 + ▾ <Suspense name="outer">
3658 + <Component key="one">
3659 + <Component key="two">
3660 + ▾ <Suspense name="inner">
3661 + <Component key="three">
3662 + [suspense-root] rects={null}
3663 + <Suspense name="outer" uniqueSuspenders={true} rects={null}>
3664 + <Suspense name="inner" uniqueSuspenders={false} rects={null}>
3665 + `);
3666 +
3667 + // We remove one suspender.
3668 + // The inner one shouldn't have unique suspenders because it's still blocked
3669 + // by the outer one.
3670 + await actAsync(() => {
3671 + render(
3672 + <React.Suspense fallback={null} name="outer">
3673 + <Component key="one" promise={promise} />
3674 + <React.Suspense fallback={null} name="inner">
3675 + <Component key="three" promise={promise} />
3676 + </React.Suspense>
3677 + </React.Suspense>,
3678 + );
3679 + });
3680 +
3681 + expect(store).toMatchInlineSnapshot(`
3682 + [root]
3683 + ▾ <Suspense name="outer">
3684 + <Component key="one">
3685 + ▾ <Suspense name="inner">
3686 + <Component key="three">
3687 + [suspense-root] rects={null}
3688 + <Suspense name="outer" uniqueSuspenders={true} rects={null}>
3689 + <Suspense name="inner" uniqueSuspenders={false} rects={null}>
3690 + `);
3691 +
3692 + // Now we remove all unique suspenders of the outer Suspense boundary.
3693 + // The inner one is now independently revealed from the parent and should
3694 + // be marked as having unique suspenders.
3695 + // TODO: The outer boundary no longer has unique suspenders.
3696 + await actAsync(() => {
3697 + render(
3698 + <React.Suspense fallback={null} name="outer">
3699 + <React.Suspense fallback={null} name="inner">
3700 + <Component key="three" promise={promise} />
3701 + </React.Suspense>
3702 + </React.Suspense>,
3703 + );
3704 + });
3705 +
3706 + expect(store).toMatchInlineSnapshot(`
3707 + [root]
3708 + ▾ <Suspense name="outer">
3709 + ▾ <Suspense name="inner">
3710 + <Component key="three">
3711 + [suspense-root] rects={null}
3712 + <Suspense name="outer" uniqueSuspenders={true} rects={null}>
3713 + <Suspense name="inner" uniqueSuspenders={true} rects={null}>
3714 + `);
3715 + });
3716 +
3717 // @reactVersion >= 19
3718 it('cleans up host hoistables', async () => {
3719 function Left() {
packages/react-devtools-shared/src/backend/fiber/renderer.js
+10 -9
@@ -3197,15 +3197,16 @@ export function attach(
3197 environmentCounts.set(env, count - 1);
3198 }
3199 }
3200 - }
3201 - if (
3202 - suspenseNode.hasUniqueSuspenders &&
3203 - !ioExistsInSuspenseAncestor(suspenseNode, ioInfo)
3204 - ) {
3205 - // This entry wasn't in any ancestor and is no longer in this suspense boundary.
3206 - // This means that a child might now be the unique suspender for this IO.
3207 - // Search the child boundaries to see if we can reveal any of them.
3208 - unblockSuspendedBy(suspenseNode, ioInfo);
3200 +
3201 + if (
3202 + suspenseNode.hasUniqueSuspenders &&
3203 + !ioExistsInSuspenseAncestor(suspenseNode, ioInfo)
3204 + ) {
3205 + // This entry wasn't in any ancestor and is no longer in this suspense boundary.
3206 + // This means that a child might now be the unique suspender for this IO.
3207 + // Search the child boundaries to see if we can reveal any of them.
3208 + unblockSuspendedBy(suspenseNode, ioInfo);
3209 + }
3210 }
3211 }
3212 }