diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 688f1b473bf62..66d1c3f69019c 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -2139,8 +2139,8 @@ export function attach( // Regular operations pendingOperations.length + // All suspender changes are batched in a single message. - // [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders]] - (numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 2 : 0), + // [SUSPENSE_TREE_OPERATION_SUSPENDERS, suspenderChangesLength, ...[id, hasUniqueSuspenders, isSuspended]] + (numSuspenderChanges > 0 ? 2 + numSuspenderChanges * 3 : 0), ); // Identify which renderer this update is coming from. @@ -2225,6 +2225,14 @@ export function attach( } operations[i++] = fiberIdWithChanges; operations[i++] = suspense.hasUniqueSuspenders ? 1 : 0; + const instance = suspense.instance; + const isSuspended = + // TODO: Track if other SuspenseNode like SuspenseList rows are suspended. + (instance.kind === FIBER_INSTANCE || + instance.kind === FILTERED_FIBER_INSTANCE) && + instance.data.tag === SuspenseComponent && + instance.data.memoizedState !== null; + operations[i++] = isSuspended ? 1 : 0; operations[i++] = suspense.environments.size; suspense.environments.forEach((count, env) => { operations[i++] = getStringID(env); @@ -2640,9 +2648,15 @@ export function attach( const fiber = fiberInstance.data; const props = fiber.memoizedProps; // TODO: Compute a fallback name based on Owner, key etc. - const name = props === null ? null : props.name || null; + const name = + fiber.tag !== SuspenseComponent || props === null + ? null + : props.name || null; const nameStringID = getStringID(name); + const isSuspended = + fiber.tag === SuspenseComponent && fiber.memoizedState !== null; + if (__DEBUG__) { console.log('recordSuspenseMount()', suspenseInstance); } @@ -2653,6 +2667,7 @@ export function attach( pushOperation(fiberID); pushOperation(parentID); pushOperation(nameStringID); + pushOperation(isSuspended ? 1 : 0); const rects = suspenseInstance.rects; if (rects === null) { @@ -5006,15 +5021,24 @@ export function attach( const nextIsSuspended = isSuspendedOffscreen(nextFiber); if (isLegacySuspense) { - if ( - fiberInstance !== null && - fiberInstance.suspenseNode !== null && - (prevFiber.stateNode === null) !== (nextFiber.stateNode === null) - ) { - trackThrownPromisesFromRetryCache( - fiberInstance.suspenseNode, - nextFiber.stateNode, - ); + if (fiberInstance !== null && fiberInstance.suspenseNode !== null) { + const suspenseNode = fiberInstance.suspenseNode; + if ( + (prevFiber.stateNode === null) !== + (nextFiber.stateNode === null) + ) { + trackThrownPromisesFromRetryCache( + suspenseNode, + nextFiber.stateNode, + ); + } + if ( + (prevFiber.memoizedState === null) !== + (nextFiber.memoizedState === null) + ) { + // Toggle suspended state. + recordSuspenseSuspenders(suspenseNode); + } } } // The logic below is inspired by the code paths in updateSuspenseComponent() @@ -5162,6 +5186,14 @@ export function attach( ); } + if ( + (prevFiber.memoizedState === null) !== + (nextFiber.memoizedState === null) + ) { + // Toggle suspended state. + recordSuspenseSuspenders(suspenseNode); + } + shouldMeasureSuspenseNode = false; updateFlags |= updateSuspenseChildrenRecursively( nextContentFiber, @@ -5188,6 +5220,8 @@ export function attach( } trackThrownPromisesFromRetryCache(suspenseNode, nextFiber.stateNode); + // Toggle suspended state. + recordSuspenseSuspenders(suspenseNode); mountSuspenseChildrenRecursively( nextContentFiber, diff --git a/packages/react-devtools-shared/src/backend/legacy/renderer.js b/packages/react-devtools-shared/src/backend/legacy/renderer.js index 46709c9a8048c..1262a8d4647a6 100644 --- a/packages/react-devtools-shared/src/backend/legacy/renderer.js +++ b/packages/react-devtools-shared/src/backend/legacy/renderer.js @@ -417,6 +417,7 @@ export function attach( pushOperation(id); pushOperation(parentID); pushOperation(getStringID(null)); // name + pushOperation(0); // isSuspended // TODO: Measure rect of root pushOperation(-1); } else { diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index eeb6da60f8aeb..86961f5bd91fb 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -1552,7 +1552,8 @@ export default class Store extends EventEmitter<{ const id = operations[i + 1]; const parentID = operations[i + 2]; const nameStringID = operations[i + 3]; - const numRects = ((operations[i + 4]: any): number); + const isSuspended = operations[i + 4] === 1; + const numRects = ((operations[i + 5]: any): number); let name = stringTable[nameStringID]; if (this._idToSuspense.has(id)) { @@ -1579,7 +1580,7 @@ export default class Store extends EventEmitter<{ } } - i += 5; + i += 6; let rects: SuspenseNode['rects']; if (numRects === -1) { rects = null; @@ -1625,6 +1626,7 @@ export default class Store extends EventEmitter<{ name, rects, hasUniqueSuspenders: false, + isSuspended: isSuspended, }); hasSuspenseTreeChanged = true; @@ -1801,6 +1803,7 @@ export default class Store extends EventEmitter<{ for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) { const id = operations[i++]; const hasUniqueSuspenders = operations[i++] === 1; + const isSuspended = operations[i++] === 1; const environmentNamesLength = operations[i++]; const environmentNames = []; for ( @@ -1832,6 +1835,7 @@ export default class Store extends EventEmitter<{ } suspense.hasUniqueSuspenders = hasUniqueSuspenders; + suspense.isSuspended = isSuspended; // TODO: Recompute the environment names. } diff --git a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js index 28be5f9e1c7e1..4addf10916693 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js @@ -378,7 +378,8 @@ function updateTree( const fiberID = operations[i + 1]; const parentID = operations[i + 2]; const nameStringID = operations[i + 3]; - const numRects = operations[i + 4]; + const isSuspended = operations[i + 4]; + const numRects = operations[i + 5]; const name = stringTable[nameStringID]; if (__DEBUG__) { @@ -388,16 +389,16 @@ function updateTree( } else { rects = '[' + - operations.slice(i + 5, i + 5 + numRects * 4).join(',') + + operations.slice(i + 6, i + 6 + numRects * 4).join(',') + ']'; } debug( 'Add suspense', - `node ${fiberID} (name=${JSON.stringify(name)}, rects={${rects}}) under ${parentID}`, + `node ${fiberID} (name=${JSON.stringify(name)}, rects={${rects}}) under ${parentID} suspended ${isSuspended}`, ); } - i += 5 + (numRects === -1 ? 0 : numRects * 4); + i += 6 + (numRects === -1 ? 0 : numRects * 4); break; } @@ -459,12 +460,13 @@ function updateTree( for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) { const suspenseNodeId = operations[i++]; const hasUniqueSuspenders = operations[i++] === 1; + const isSuspended = operations[i++] === 1; const environmentNamesLength = operations[i++]; i += environmentNamesLength; if (__DEBUG__) { debug( 'Suspender changes', - `Suspense node ${suspenseNodeId} unique suspenders set to ${String(hasUniqueSuspenders)} with ${String(environmentNamesLength)} environments`, + `Suspense node ${suspenseNodeId} unique suspenders set to ${String(hasUniqueSuspenders)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`, ); } } diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css index ba862051d9513..a08b5f8c8a358 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css @@ -49,6 +49,10 @@ outline-width: 0; } +.SuspenseRectsScaledRect[data-suspended='true'] { + opacity: 0.3; +} + /* highlight this boundary */ .SuspenseRectsBoundary:hover:not(:has(.SuspenseRectsBoundary:hover)) > .SuspenseRectsRect, .SuspenseRectsBoundary[data-highlighted='true'] > .SuspenseRectsRect { background-color: var(--color-background-hover); diff --git a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js index f949631be8427..5f07bb61001ee 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js @@ -35,11 +35,13 @@ function ScaledRect({ className, rect, visible, + suspended, ...props }: { className: string, rect: Rect, visible: boolean, + suspended: boolean, ... }): React$Node { const viewBox = useContext(ViewBox); @@ -53,6 +55,7 @@ function ScaledRect({ {...props} className={styles.SuspenseRectsScaledRect + ' ' + className} data-visible={visible} + data-suspended={suspended} style={{ width, height, @@ -145,7 +148,8 @@ function SuspenseRects({ + visible={visible} + suspended={suspense.isSuspended}> {visible && suspense.rects !== null && diff --git a/packages/react-devtools-shared/src/frontend/types.js b/packages/react-devtools-shared/src/frontend/types.js index 7762af43e0040..2a012ce33a17d 100644 --- a/packages/react-devtools-shared/src/frontend/types.js +++ b/packages/react-devtools-shared/src/frontend/types.js @@ -200,6 +200,7 @@ export type SuspenseNode = { name: string | null, rects: null | Array, hasUniqueSuspenders: boolean, + isSuspended: boolean, }; // Serialized version of ReactIOInfo diff --git a/packages/react-devtools-shared/src/utils.js b/packages/react-devtools-shared/src/utils.js index 007db77f2202a..29ff6d566bd6f 100644 --- a/packages/react-devtools-shared/src/utils.js +++ b/packages/react-devtools-shared/src/utils.js @@ -340,9 +340,10 @@ export function printOperationsArray(operations: Array) { const fiberID = operations[i + 1]; const parentID = operations[i + 2]; const nameStringID = operations[i + 3]; - const numRects = operations[i + 4]; + const isSuspended = operations[i + 4]; + const numRects = operations[i + 5]; - i += 5; + i += 6; const name = stringTable[nameStringID]; let rects: string; @@ -368,7 +369,7 @@ export function printOperationsArray(operations: Array) { } logs.push( - `Add suspense node ${fiberID} (${String(name)},rects={${rects}}) under ${parentID}`, + `Add suspense node ${fiberID} (${String(name)},rects={${rects}}) under ${parentID} suspended ${isSuspended}`, ); break; } @@ -431,10 +432,11 @@ export function printOperationsArray(operations: Array) { for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) { const id = operations[i++]; const hasUniqueSuspenders = operations[i++] === 1; + const isSuspended = operations[i++] === 1; const environmentNamesLength = operations[i++]; i += environmentNamesLength; logs.push( - `Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} with ${String(environmentNamesLength)} environments`, + `Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} is suspended set to ${String(isSuspended)} with ${String(environmentNamesLength)} environments`, ); }