@samitouri / QOS-React-1 / commits / dcf2a6f665

[DevTools] Keep a Suspense Tree Parellel to the Instance tree in the Backend (#34050)

This keeps a data structure of Suspense boundaries and the root which can keep track which boundaries might participate in a loading sequence and everything that suspends them. This will power the Suspense tab. Now when you select a `<Suspense>` boundary the "suspended by" section shows the whole boundary instead of just that component. In the future, we'll likely need to add "Activity" boundaries to this tree as well, so that we can track what suspended the root of an Activity when filtering a subtree. Similar to how the root SuspenseNode now tracks suspending at the root. Maybe it's ok to just traverse to collect this information on-demand when you select one though since this doesn't contribute to the deduping. We'll also need to add implicit Suspense boundaries for the rows of a SuspenseList with `tail=hidden/collapsed`.

Sebastian Markbåge committed Jul 30, 2025 at 09:55 UTC dcf2a6f6651c0791c2e87253a8815fcf3d53e4d2
1 file changed +340 -18
packages/react-devtools-shared/src/backend/fiber/renderer.js
+340 -18
@@ -11,6 +11,7 @@ import type {
11 ReactComponentInfo,
12 ReactDebugInfo,
13 ReactAsyncInfo,
14 + ReactIOInfo,
15 } from 'shared/ReactTypes';
16
17 import {
@@ -173,6 +174,7 @@ type FiberInstance = {
174 logCount: number, // total number of errors/warnings last seen
175 treeBaseDuration: number, // the profiled time of the last render of this subtree
176 suspendedBy: null | Array<ReactAsyncInfo>, // things that suspended in the children position of this component
177 + suspenseNode: null | SuspenseNode,
178 data: Fiber, // one of a Fiber pair
179 };
180
@@ -187,6 +189,7 @@ function createFiberInstance(fiber: Fiber): FiberInstance {
189 logCount: 0,
190 treeBaseDuration: 0,
191 suspendedBy: null,
192 + suspenseNode: null,
193 data: fiber,
194 };
195 }
@@ -203,6 +206,7 @@ type FilteredFiberInstance = {
206 logCount: number, // total number of errors/warnings last seen
207 treeBaseDuration: number, // the profiled time of the last render of this subtree
208 suspendedBy: null | Array<ReactAsyncInfo>, // not used
209 + suspenseNode: null | SuspenseNode,
210 data: Fiber, // one of a Fiber pair
211 };
212
@@ -218,6 +222,7 @@ function createFilteredFiberInstance(fiber: Fiber): FilteredFiberInstance {
222 logCount: 0,
223 treeBaseDuration: 0,
224 suspendedBy: null,
225 + suspenseNode: null,
226 data: fiber,
227 }: any);
228 }
@@ -237,6 +242,7 @@ type VirtualInstance = {
242 logCount: number, // total number of errors/warnings last seen
243 treeBaseDuration: number, // the profiled time of the last render of this subtree
244 suspendedBy: null | Array<ReactAsyncInfo>, // things that blocked the server component's child from rendering
245 + suspenseNode: null,
246 // The latest info for this instance. This can be updated over time and the
247 // same info can appear in more than once ServerComponentInstance.
248 data: ReactComponentInfo,
@@ -255,12 +261,39 @@ function createVirtualInstance(
261 logCount: 0,
262 treeBaseDuration: 0,
263 suspendedBy: null,
264 + suspenseNode: null,
265 data: debugEntry,
266 };
267 }
268
269 type DevToolsInstance = FiberInstance | VirtualInstance | FilteredFiberInstance;
270
271 +type SuspenseNode = {
272 + // The Instance can be a Suspense boundary, a SuspenseList Row, or HostRoot.
273 + // It can also be disconnected from the main tree if it's a Filtered Instance.
274 + instance: FiberInstance | FilteredFiberInstance,
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.
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,
282 +};
283 +
284 +function createSuspenseNode(
285 + instance: FiberInstance | FilteredFiberInstance,
286 +): SuspenseNode {
287 + return (instance.suspenseNode = {
288 + instance: instance,
289 + parent: null,
290 + firstChild: null,
291 + nextSibling: null,
292 + suspendedBy: new Map(),
293 + hasUniqueSuspenders: false,
294 + });
295 +}
296 +
297 type getDisplayNameForFiberType = (fiber: Fiber) => string | null;
298 type getTypeSymbolType = (type: any) => symbol | string | number;
299
@@ -2367,18 +2400,162 @@ export function attach(
2400 // the current parent here as well.
2401 let reconcilingParent: null | DevToolsInstance = null;
2402
2403 + let remainingReconcilingChildrenSuspenseNodes: null | SuspenseNode = null;
2404 + // The previously placed child.
2405 + let previouslyReconciledSiblingSuspenseNode: null | SuspenseNode = null;
2406 + // To save on stack allocation and ensure that they are updated as a pair, we also store
2407 + // the current parent here as well.
2408 + let reconcilingParentSuspenseNode: null | SuspenseNode = null;
2409 +
2410 + function isSuspenseInFallback(suspenseNode: SuspenseNode) {
2411 + const fiber = suspenseNode.instance.data;
2412 + return fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
2413 + }
2414 +
2415 + function ioExistsInSuspenseAncestor(
2416 + suspenseNode: SuspenseNode,
2417 + ioInfo: ReactIOInfo,
2418 + ): boolean {
2419 + let ancestor = suspenseNode.parent;
2420 + while (ancestor !== null) {
2421 + if (ancestor.suspendedBy.has(ioInfo)) {
2422 + return true;
2423 + }
2424 + ancestor = ancestor.parent;
2425 + }
2426 + return false;
2427 + }
2428 +
2429 function insertSuspendedBy(asyncInfo: ReactAsyncInfo): void {
2430 + let parentSuspenseNode = reconcilingParentSuspenseNode;
2431 + while (
2432 + parentSuspenseNode !== null &&
2433 + isSuspenseInFallback(parentSuspenseNode)
2434 + ) {
2435 + // If we have something that suspends inside the fallback tree of a Suspense boundary, then
2436 + // we bubble that up to the nearest parent Suspense boundary that isn't in fallback mode.
2437 + parentSuspenseNode = parentSuspenseNode.parent;
2438 + }
2439 const parentInstance = reconcilingParent;
2372 - if (parentInstance === null) {
2373 - // Suspending at the root is not attributed to any particular component
2374 - // TODO: It should be attributed to the shell.
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 + }
2461 + }
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);
2469 + }
2470 + }
2471 + }
2472 +
2473 + function getAwaitInSuspendedByFromIO(
2474 + suspensedBy: Array<ReactAsyncInfo>,
2475 + ioInfo: ReactIOInfo,
2476 + ): null | ReactAsyncInfo {
2477 + for (let i = 0; i < suspensedBy.length; i++) {
2478 + const asyncInfo = suspensedBy[i];
2479 + if (asyncInfo.awaited === ioInfo) {
2480 + return asyncInfo;
2481 + }
2482 + }
2483 + return null;
2484 + }
2485 +
2486 + function unblockSuspendedBy(
2487 + parentSuspenseNode: SuspenseNode,
2488 + ioInfo: ReactIOInfo,
2489 + ): void {
2490 + const firstChild = parentSuspenseNode.firstChild;
2491 + if (firstChild === null) {
2492 return;
2493 }
2377 - const suspendedBy = parentInstance.suspendedBy;
2378 - if (suspendedBy === null) {
2379 - parentInstance.suspendedBy = [asyncInfo];
2380 - } else if (suspendedBy.indexOf(asyncInfo) === -1) {
2381 - suspendedBy.push(asyncInfo);
2494 + let node: SuspenseNode = firstChild;
2495 + while (node !== null) {
2496 + if (node.suspendedBy.has(ioInfo)) {
2497 + // We have found a child boundary that depended on the unblocked I/O.
2498 + // It can now be marked as having unique suspenders. We can skip its children
2499 + // since they'll still be blocked by this one.
2500 + node.hasUniqueSuspenders = true;
2501 + } else if (node.firstChild !== null) {
2502 + node = node.firstChild;
2503 + continue;
2504 + }
2505 + while (node.nextSibling === null) {
2506 + if (node.parent === null || node.parent === parentSuspenseNode) {
2507 + return;
2508 + }
2509 + node = node.parent;
2510 + }
2511 + node = node.nextSibling;
2512 + }
2513 + }
2514 +
2515 + function removePreviousSuspendedBy(
2516 + instance: DevToolsInstance,
2517 + previousSuspendedBy: null | Array<ReactAsyncInfo>,
2518 + ): void {
2519 + // Remove any async info from the parent, if they were in the previous set but
2520 + // is no longer in the new set.
2521 + const parentSuspenseNode = reconcilingParentSuspenseNode;
2522 + if (previousSuspendedBy !== null && parentSuspenseNode !== null) {
2523 + const nextSuspendedBy = instance.suspendedBy;
2524 + for (let i = 0; i < previousSuspendedBy.length; i++) {
2525 + const asyncInfo = previousSuspendedBy[i];
2526 + if (
2527 + nextSuspendedBy === null ||
2528 + (nextSuspendedBy.indexOf(asyncInfo) === -1 &&
2529 + getAwaitInSuspendedByFromIO(nextSuspendedBy, asyncInfo.awaited) ===
2530 + null)
2531 + ) {
2532 + // This IO entry is no longer blocking the current tree.
2533 + // Let's remove it from the parent SuspenseNode.
2534 + const ioInfo = asyncInfo.awaited;
2535 + const suspendedBySet = parentSuspenseNode.suspendedBy.get(ioInfo);
2536 + if (
2537 + suspendedBySet === undefined ||
2538 + !suspendedBySet.delete(instance)
2539 + ) {
2540 + throw new Error(
2541 + 'We are cleaning up async info that was not on the parent Suspense boundary. ' +
2542 + 'This is a bug in React.',
2543 + );
2544 + }
2545 + if (suspendedBySet.size === 0) {
2546 + parentSuspenseNode.suspendedBy.delete(asyncInfo.awaited);
2547 + }
2548 + if (
2549 + parentSuspenseNode.hasUniqueSuspenders &&
2550 + !ioExistsInSuspenseAncestor(parentSuspenseNode, ioInfo)
2551 + ) {
2552 + // This entry wasn't in any ancestor and is no longer in this suspense boundary.
2553 + // This means that a child might now be the unique suspender for this IO.
2554 + // Search the child boundaries to see if we can reveal any of them.
2555 + unblockSuspendedBy(parentSuspenseNode, ioInfo);
2556 + }
2557 + }
2558 + }
2559 }
2560 }
2561
@@ -2398,6 +2575,22 @@ export function attach(
2575 previouslyReconciledSibling = instance;
2576 }
2577 instance.nextSibling = null;
2578 + // Insert any SuspenseNode into its parent Node.
2579 + const suspenseNode = instance.suspenseNode;
2580 + if (suspenseNode !== null) {
2581 + const parentNode = reconcilingParentSuspenseNode;
2582 + if (parentNode !== null) {
2583 + suspenseNode.parent = parentNode;
2584 + if (previouslyReconciledSiblingSuspenseNode === null) {
2585 + previouslyReconciledSiblingSuspenseNode = suspenseNode;
2586 + parentNode.firstChild = suspenseNode;
2587 + } else {
2588 + previouslyReconciledSiblingSuspenseNode.nextSibling = suspenseNode;
2589 + previouslyReconciledSiblingSuspenseNode = suspenseNode;
2590 + }
2591 + suspenseNode.nextSibling = null;
2592 + }
2593 + }
2594 }
2595
2596 function moveChild(
@@ -2447,6 +2640,36 @@ export function attach(
2640 }
2641 instance.nextSibling = null;
2642 instance.parent = null;
2643 +
2644 + // Remove any SuspenseNode from its parent.
2645 + const suspenseNode = instance.suspenseNode;
2646 + if (suspenseNode !== null && suspenseNode.parent !== null) {
2647 + const parentNode = reconcilingParentSuspenseNode;
2648 + if (parentNode === null) {
2649 + throw new Error('Should not have a parent if we are at the root');
2650 + }
2651 + if (suspenseNode.parent !== parentNode) {
2652 + throw new Error(
2653 + 'Cannot remove a node from a different parent than is being reconciled.',
2654 + );
2655 + }
2656 + let previousSuspenseSibling = remainingReconcilingChildrenSuspenseNodes;
2657 + if (previousSuspenseSibling === suspenseNode) {
2658 + // We're first in the remaining set. Remove us.
2659 + remainingReconcilingChildrenSuspenseNodes = suspenseNode.nextSibling;
2660 + } else {
2661 + // Search for our previous sibling and remove us.
2662 + while (previousSuspenseSibling !== null) {
2663 + if (previousSuspenseSibling.nextSibling === suspenseNode) {
2664 + previousSuspenseSibling.nextSibling = suspenseNode.nextSibling;
2665 + break;
2666 + }
2667 + previousSuspenseSibling = previousSuspenseSibling.nextSibling;
2668 + }
2669 + }
2670 + suspenseNode.nextSibling = null;
2671 + suspenseNode.parent = null;
2672 + }
2673 }
2674
2675 function unmountRemainingChildren() {
@@ -2654,20 +2877,27 @@ export function attach(
2877 ): void {
2878 const shouldIncludeInTree = !shouldFilterFiber(fiber);
2879 let newInstance = null;
2880 + let newSuspenseNode = null;
2881 if (shouldIncludeInTree) {
2882 newInstance = recordMount(fiber, reconcilingParent);
2883 + if (fiber.tag === SuspenseComponent || fiber.tag === HostRoot) {
2884 + newSuspenseNode = createSuspenseNode(newInstance);
2885 + }
2886 insertChild(newInstance);
2887 if (__DEBUG__) {
2888 debug('mountFiberRecursively()', newInstance, reconcilingParent);
2889 }
2890 } else if (
2664 - reconcilingParent !== null &&
2665 - reconcilingParent.kind === VIRTUAL_INSTANCE
2891 + (reconcilingParent !== null &&
2892 + reconcilingParent.kind === VIRTUAL_INSTANCE) ||
2893 + fiber.tag === SuspenseComponent
2894 ) {
2895 // If the parent is a Virtual Instance and we filtered this Fiber we include a
2668 - // hidden node.
2669 -
2896 + // hidden node. We also include this if it's a Suspense boundary so we can track those
2897 + // in the Suspense tree.
2898 if (
2899 + reconcilingParent !== null &&
2900 + reconcilingParent.kind === VIRTUAL_INSTANCE &&
2901 reconcilingParent.data === fiber._debugOwner &&
2902 fiber._debugStack != null &&
2903 reconcilingParent.source === null
@@ -2678,6 +2908,9 @@ export function attach(
2908 }
2909
2910 newInstance = createFilteredFiberInstance(fiber);
2911 + if (fiber.tag === SuspenseComponent) {
2912 + newSuspenseNode = createSuspenseNode(newInstance);
2913 + }
2914 insertChild(newInstance);
2915 if (__DEBUG__) {
2916 debug('mountFiberRecursively()', newInstance, reconcilingParent);
@@ -2694,12 +2927,20 @@ export function attach(
2927 const stashedParent = reconcilingParent;
2928 const stashedPrevious = previouslyReconciledSibling;
2929 const stashedRemaining = remainingReconcilingChildren;
2930 + const stashedSuspenseParent = reconcilingParentSuspenseNode;
2931 + const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode;
2932 + const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes;
2933 if (newInstance !== null) {
2934 // Push a new DevTools instance parent while reconciling this subtree.
2935 reconcilingParent = newInstance;
2936 previouslyReconciledSibling = null;
2937 remainingReconcilingChildren = null;
2938 }
2939 + if (newSuspenseNode !== null) {
2940 + reconcilingParentSuspenseNode = newSuspenseNode;
2941 + previouslyReconciledSiblingSuspenseNode = null;
2942 + remainingReconcilingChildrenSuspenseNodes = null;
2943 + }
2944 try {
2945 if (traceUpdatesEnabled) {
2946 if (traceNearestHostComponentUpdate) {
@@ -2753,6 +2994,7 @@ export function attach(
2994 );
2995 }
2996 }
2997 + // TODO: Track SuspenseNode in resuspended trees.
2998 } else {
2999 let primaryChild: Fiber | null = null;
3000 const areSuspenseChildrenConditionallyWrapped =
@@ -2784,6 +3026,11 @@ export function attach(
3026 previouslyReconciledSibling = stashedPrevious;
3027 remainingReconcilingChildren = stashedRemaining;
3028 }
3029 + if (newSuspenseNode !== null) {
3030 + reconcilingParentSuspenseNode = stashedSuspenseParent;
3031 + previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3032 + remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3033 + }
3034 }
3035
3036 // We're exiting this Fiber now, and entering its siblings.
@@ -2801,6 +3048,10 @@ export function attach(
3048 const stashedParent = reconcilingParent;
3049 const stashedPrevious = previouslyReconciledSibling;
3050 const stashedRemaining = remainingReconcilingChildren;
3051 + const stashedSuspenseParent = reconcilingParentSuspenseNode;
3052 + const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode;
3053 + const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes;
3054 + const previousSuspendedBy = instance.suspendedBy;
3055 // Push a new DevTools instance parent while reconciling this subtree.
3056 reconcilingParent = instance;
3057 previouslyReconciledSibling = null;
@@ -2808,13 +3059,24 @@ export function attach(
3059 remainingReconcilingChildren = instance.firstChild;
3060 instance.firstChild = null;
3061 instance.suspendedBy = null;
3062 +
3063 + if (instance.suspenseNode !== null) {
3064 + reconcilingParentSuspenseNode = instance.suspenseNode;
3065 + previouslyReconciledSiblingSuspenseNode = null;
3066 + remainingReconcilingChildrenSuspenseNodes = null;
3067 + }
3068 +
3069 try {
3070 // Unmount the remaining set.
3071 unmountRemainingChildren();
3072 + removePreviousSuspendedBy(instance, previousSuspendedBy);
3073 } finally {
3074 reconcilingParent = stashedParent;
3075 previouslyReconciledSibling = stashedPrevious;
3076 remainingReconcilingChildren = stashedRemaining;
3077 + reconcilingParentSuspenseNode = stashedSuspenseParent;
3078 + previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3079 + remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3080 }
3081 if (instance.kind === FIBER_INSTANCE) {
3082 recordUnmount(instance);
@@ -3001,6 +3263,7 @@ export function attach(
3263 const stashedParent = reconcilingParent;
3264 const stashedPrevious = previouslyReconciledSibling;
3265 const stashedRemaining = remainingReconcilingChildren;
3266 + const previousSuspendedBy = virtualInstance.suspendedBy;
3267 // Push a new DevTools instance parent while reconciling this subtree.
3268 reconcilingParent = virtualInstance;
3269 previouslyReconciledSibling = null;
@@ -3021,6 +3284,7 @@ export function attach(
3284 ) {
3285 recordResetChildren(virtualInstance);
3286 }
3287 + removePreviousSuspendedBy(virtualInstance, previousSuspendedBy);
3288 // Update the errors/warnings count. If this Instance has switched to a different
3289 // ReactComponentInfo instance, such as when refreshing Server Components, then
3290 // we replace all the previous logs with the ones associated with the new ones rather
@@ -3376,7 +3640,12 @@ export function attach(
3640 const stashedParent = reconcilingParent;
3641 const stashedPrevious = previouslyReconciledSibling;
3642 const stashedRemaining = remainingReconcilingChildren;
3643 + const stashedSuspenseParent = reconcilingParentSuspenseNode;
3644 + const stashedSuspensePrevious = previouslyReconciledSiblingSuspenseNode;
3645 + const stashedSuspenseRemaining = remainingReconcilingChildrenSuspenseNodes;
3646 + let previousSuspendedBy = null;
3647 if (fiberInstance !== null) {
3648 + previousSuspendedBy = fiberInstance.suspendedBy;
3649 // Update the Fiber so we that we always keep the current Fiber on the data.
3650 fiberInstance.data = nextFiber;
3651 if (
@@ -3396,6 +3665,12 @@ export function attach(
3665 remainingReconcilingChildren = fiberInstance.firstChild;
3666 fiberInstance.firstChild = null;
3667 fiberInstance.suspendedBy = null;
3668 +
3669 + if (fiberInstance.suspenseNode !== null) {
3670 + reconcilingParentSuspenseNode = fiberInstance.suspenseNode;
3671 + previouslyReconciledSiblingSuspenseNode = null;
3672 + remainingReconcilingChildrenSuspenseNodes = null;
3673 + }
3674 }
3675 try {
3676 if (
@@ -3550,6 +3825,8 @@ export function attach(
3825 }
3826
3827 if (fiberInstance !== null) {
3828 + removePreviousSuspendedBy(fiberInstance, previousSuspendedBy);
3829 +
3830 let componentLogsEntry = fiberToComponentLogsMap.get(
3831 fiberInstance.data,
3832 );
@@ -3587,6 +3864,9 @@ export function attach(
3864 reconcilingParent = stashedParent;
3865 previouslyReconciledSibling = stashedPrevious;
3866 remainingReconcilingChildren = stashedRemaining;
3867 + reconcilingParentSuspenseNode = stashedSuspenseParent;
3868 + previouslyReconciledSiblingSuspenseNode = stashedSuspensePrevious;
3869 + remainingReconcilingChildrenSuspenseNodes = stashedSuspenseRemaining;
3870 }
3871 }
3872 }
@@ -4104,6 +4384,43 @@ export function attach(
4384 return null;
4385 }
4386
4387 + function getSuspendedByOfSuspenseNode(
4388 + suspenseNode: SuspenseNode,
4389 + ): Array<ReactAsyncInfo> {
4390 + // Collect all ReactAsyncInfo that was suspending this SuspenseNode but
4391 + // isn't also in any parent set.
4392 + const result: Array<ReactAsyncInfo> = [];
4393 + if (!suspenseNode.hasUniqueSuspenders) {
4394 + return result;
4395 + }
4396 + suspenseNode.suspendedBy.forEach((set, ioInfo) => {
4397 + let parentNode = suspenseNode.parent;
4398 + while (parentNode !== null) {
4399 + if (parentNode.suspendedBy.has(ioInfo)) {
4400 + return;
4401 + }
4402 + parentNode = parentNode.parent;
4403 + }
4404 + // We have the ioInfo but we need to find at least one corresponding await
4405 + // to go along with it. We don't really need to show every child that awaits the same
4406 + // thing so we just pick the first one that is still alive.
4407 + if (set.size === 0) {
4408 + return;
4409 + }
4410 + const firstInstance: DevToolsInstance = (set.values().next().value: any);
4411 + if (firstInstance.suspendedBy !== null) {
4412 + const asyncInfo = getAwaitInSuspendedByFromIO(
4413 + firstInstance.suspendedBy,
4414 + ioInfo,
4415 + );
4416 + if (asyncInfo !== null) {
4417 + result.push(asyncInfo);
4418 + }
4419 + }
4420 + });
4421 + return result;
4422 + }
4423 +
4424 function serializeAsyncInfo(
4425 asyncInfo: ReactAsyncInfo,
4426 index: number,
@@ -4448,12 +4765,17 @@ export function attach(
4765 nativeTag = getNativeTag(fiber.stateNode);
4766 }
4767
4451 - // This set is an edge case where if you pass a promise to a Client Component into a children
4452 - // position without a Server Component as the direct parent. E.g. <div>{promise}</div>
4453 - // In this case, this becomes associated with the Client/Host Component where as normally
4454 - // you'd expect these to be associated with the Server Component that awaited the data.
4455 - // TODO: Prepend other suspense sources like css, images and use().
4456 - const suspendedBy = fiberInstance.suspendedBy;
4768 + const suspendedBy =
4769 + fiberInstance.suspenseNode !== null
4770 + ? // If this is a Suspense boundary, then we include everything in the subtree that might suspend
4771 + // this boundary down to the next Suspense boundary.
4772 + getSuspendedByOfSuspenseNode(fiberInstance.suspenseNode)
4773 + : // This set is an edge case where if you pass a promise to a Client Component into a children
4774 + // position without a Server Component as the direct parent. E.g. <div>{promise}</div>
4775 + // In this case, this becomes associated with the Client/Host Component where as normally
4776 + // you'd expect these to be associated with the Server Component that awaited the data.
4777 + // TODO: Prepend other suspense sources like css, images and use().
4778 + fiberInstance.suspendedBy;
4779
4780 return {
4781 id: fiberInstance.id,