@samitouri / QOS-React-2 / commits / 9784cb379e

[DevTools] No suspending above the root (#34055)

Follow up to #34050. It's not actually possible to suspend *above* the root since even if you suspend in the first child position, you're still suspending the HostRoot which always has a corresponding FiberInstance and SuspenseNode.

Sebastian Markbåge committed Jul 30, 2025 at 11:31 UTC 9784cb379e249a5495cde5ba3037521207144e91
1 file changed +32 -29
packages/react-devtools-shared/src/backend/fiber/renderer.js
+32 -29
@@ -275,7 +275,7 @@ type SuspenseNode = {
275 parent: null | SuspenseNode,
276 firstChild: null | SuspenseNode,
277 nextSibling: null | SuspenseNode,
278 - suspendedBy: Map<ReactIOInfo, Set<DevToolsInstance | null>>, // Tracks which data we're suspended by and the children that suspend it.
278 + suspendedBy: Map<ReactIOInfo, Set<DevToolsInstance>>, // Tracks which data we're suspended by and the children that suspend it.
279 // Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all
280 // also in the parent sets. This determine whether this could contribute in the loading sequence.
281 hasUniqueSuspenders: boolean,
@@ -2437,37 +2437,40 @@ export function attach(
2437 parentSuspenseNode = parentSuspenseNode.parent;
2438 }
2439 const parentInstance = reconcilingParent;
2440 - if (parentSuspenseNode !== null) {
2441 - const suspendedBy = parentSuspenseNode.suspendedBy;
2442 - const ioInfo = asyncInfo.awaited;
2443 - let suspendedBySet = suspendedBy.get(ioInfo);
2444 - if (suspendedBySet === undefined) {
2445 - suspendedBySet = new Set();
2446 - suspendedBy.set(asyncInfo.awaited, suspendedBySet);
2447 - }
2448 - // The child of the Suspense boundary that was suspended on this, or null if suspended at the root.
2449 - // This is used to keep track of how many dependents are still alive and also to get information
2450 - // like owner instances to link down into the tree.
2451 - if (!suspendedBySet.has(parentInstance)) {
2452 - suspendedBySet.add(parentInstance);
2453 - if (
2454 - !parentSuspenseNode.hasUniqueSuspenders &&
2455 - !ioExistsInSuspenseAncestor(parentSuspenseNode, ioInfo)
2456 - ) {
2457 - // This didn't exist in the parent before, so let's mark this boundary as having a unique suspender.
2458 - parentSuspenseNode.hasUniqueSuspenders = true;
2459 - }
2460 - }
2440 + if (parentInstance === null || parentSuspenseNode === null) {
2441 + throw new Error(
2442 + 'It should not be possible to have suspended data outside the root. ' +
2443 + 'Even suspending at the first position is still a child of the root.',
2444 + );
2445 }
2462 - if (parentInstance !== null) {
2463 - // Suspending at the root is not attributed to any particular component other than the SuspenseNode.
2464 - const suspendedBy = parentInstance.suspendedBy;
2465 - if (suspendedBy === null) {
2466 - parentInstance.suspendedBy = [asyncInfo];
2467 - } else if (suspendedBy.indexOf(asyncInfo) === -1) {
2468 - suspendedBy.push(asyncInfo);
2446 + const suspenseNodeSuspendedBy = parentSuspenseNode.suspendedBy;
2447 + const ioInfo = asyncInfo.awaited;
2448 + let suspendedBySet = suspenseNodeSuspendedBy.get(ioInfo);
2449 + if (suspendedBySet === undefined) {
2450 + suspendedBySet = new Set();
2451 + suspenseNodeSuspendedBy.set(asyncInfo.awaited, suspendedBySet);
2452 + }
2453 + // The child of the Suspense boundary that was suspended on this, or null if suspended at the root.
2454 + // This is used to keep track of how many dependents are still alive and also to get information
2455 + // like owner instances to link down into the tree.
2456 + if (!suspendedBySet.has(parentInstance)) {
2457 + suspendedBySet.add(parentInstance);
2458 + if (
2459 + !parentSuspenseNode.hasUniqueSuspenders &&
2460 + !ioExistsInSuspenseAncestor(parentSuspenseNode, ioInfo)
2461 + ) {
2462 + // This didn't exist in the parent before, so let's mark this boundary as having a unique suspender.
2463 + parentSuspenseNode.hasUniqueSuspenders = true;
2464 }
2465 }
2466 + // Suspending right below the root is not attributed to any particular component in UI
2467 + // other than the SuspenseNode and the HostRoot's FiberInstance.
2468 + const suspendedBy = parentInstance.suspendedBy;
2469 + if (suspendedBy === null) {
2470 + parentInstance.suspendedBy = [asyncInfo];
2471 + } else if (suspendedBy.indexOf(asyncInfo) === -1) {
2472 + suspendedBy.push(asyncInfo);
2473 + }
2474 }
2475
2476 function getAwaitInSuspendedByFromIO(