diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index f2b6700163719..c03b657e60f86 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -299,6 +299,7 @@ type SuspenseNode = { nextSibling: null | SuspenseNode, rects: null | Array, // The bounding rects of content children. suspendedBy: Map>, // Tracks which data we're suspended by and the children that suspend it. + environments: Map, // Tracks the Flight environment names that suspended this. I.e. if the server blocked this. // Track whether any of the items in suspendedBy are unique this this Suspense boundaries or if they're all // also in the parent sets. This determine whether this could contribute in the loading sequence. hasUniqueSuspenders: boolean, @@ -327,6 +328,7 @@ function createSuspenseNode( nextSibling: null, rects: null, suspendedBy: new Map(), + environments: new Map(), hasUniqueSuspenders: false, hasUnknownSuspenders: false, }); @@ -2220,6 +2222,10 @@ export function attach( } operations[i++] = fiberIdWithChanges; operations[i++] = suspense.hasUniqueSuspenders ? 1 : 0; + operations[i++] = suspense.environments.size; + suspense.environments.forEach((count, env) => { + operations[i++] = getStringID(env); + }); }); } @@ -2725,6 +2731,13 @@ export function attach( return; } + // TODO: Just enqueue the operations here instead of stashing by id. + + // Ensure each environment gets recorded in the string table since it is emitted + // before we loop it over again later during flush. + suspenseNode.environments.forEach((count, env) => { + getStringID(env); + }); pendingSuspenderChanges.add(fiberInstance.id); } @@ -2807,7 +2820,20 @@ export function attach( let suspendedBySet = suspenseNodeSuspendedBy.get(ioInfo); if (suspendedBySet === undefined) { suspendedBySet = new Set(); - suspenseNodeSuspendedBy.set(asyncInfo.awaited, suspendedBySet); + suspenseNodeSuspendedBy.set(ioInfo, suspendedBySet); + // We've added a dependency. We must increment the ref count of the environment. + const env = ioInfo.env; + if (env != null) { + const environmentCounts = parentSuspenseNode.environments; + const count = environmentCounts.get(env); + if (count === undefined || count === 0) { + environmentCounts.set(env, 1); + // We've discovered a new environment for this SuspenseNode. We'll to update the node. + recordSuspenseSuspenders(parentSuspenseNode); + } else { + environmentCounts.set(env, count + 1); + } + } } // The child of the Suspense boundary that was suspended on this, or null if suspended at the root. // This is used to keep track of how many dependents are still alive and also to get information @@ -2897,6 +2923,7 @@ export function attach( : instance.suspenseNode; if (previousSuspendedBy !== null && suspenseNode !== null) { const nextSuspendedBy = instance.suspendedBy; + let changedEnvironment = false; for (let i = 0; i < previousSuspendedBy.length; i++) { const asyncInfo = previousSuspendedBy[i]; if ( @@ -2935,7 +2962,26 @@ export function attach( } } if (suspendedBySet !== undefined && suspendedBySet.size === 0) { - suspenseNode.suspendedBy.delete(asyncInfo.awaited); + suspenseNode.suspendedBy.delete(ioInfo); + // Successfully removed all dependencies. We can decrement the ref count of the environment. + const env = ioInfo.env; + if (env != null) { + const environmentCounts = suspenseNode.environments; + const count = environmentCounts.get(env); + if (count === undefined || count === 0) { + throw new Error( + 'We are removing an environment but it was not in the set. ' + + 'This is a bug in React.', + ); + } + if (count === 1) { + environmentCounts.delete(env); + // Last one. We've now change the set of environments. We'll need to update the node. + changedEnvironment = true; + } else { + environmentCounts.set(env, count - 1); + } + } } if ( suspenseNode.hasUniqueSuspenders && @@ -2948,6 +2994,9 @@ export function attach( } } } + if (changedEnvironment) { + recordSuspenseSuspenders(suspenseNode); + } } } diff --git a/packages/react-devtools-shared/src/devtools/store.js b/packages/react-devtools-shared/src/devtools/store.js index 79e6957aecfe4..6ffbb52d94e8e 100644 --- a/packages/react-devtools-shared/src/devtools/store.js +++ b/packages/react-devtools-shared/src/devtools/store.js @@ -1759,12 +1759,22 @@ export default class Store extends EventEmitter<{ break; } case SUSPENSE_TREE_OPERATION_SUSPENDERS: { - const changeLength = operations[i + 1]; - i += 2; + i++; + const changeLength = operations[i++]; for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) { - const id = operations[i]; - const hasUniqueSuspenders = operations[i + 1] === 1; + const id = operations[i++]; + const hasUniqueSuspenders = operations[i++] === 1; + const environmentNamesLength = operations[i++]; + const environmentNames = []; + for ( + let envIndex = 0; + envIndex < environmentNamesLength; + envIndex++ + ) { + const environmentNameStringID = operations[i++]; + environmentNames.push(stringTable[environmentNameStringID]); + } const suspense = this._idToSuspense.get(id); if (suspense === undefined) { @@ -1777,8 +1787,6 @@ export default class Store extends EventEmitter<{ break; } - i += 2; - if (__DEBUG__) { const previousHasUniqueSuspenders = suspense.hasUniqueSuspenders; debug( @@ -1788,6 +1796,7 @@ export default class Store extends EventEmitter<{ } suspense.hasUniqueSuspenders = hasUniqueSuspenders; + // TODO: Recompute the environment names. } hasSuspenseTreeChanged = true; 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 5637967a6abb2..9e928b0231985 100644 --- a/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js +++ b/packages/react-devtools-shared/src/devtools/views/Profiler/CommitTreeBuilder.js @@ -454,14 +454,22 @@ function updateTree( } case SUSPENSE_TREE_OPERATION_SUSPENDERS: { - const changesLength = ((operations[i + 1]: any): number); - - if (__DEBUG__) { - const changes = operations.slice(i + 2, i + 2 + changesLength * 2); - debug('Suspender changes', `[${changes.join(',')}]`); + i++; + const changeLength = ((operations[i++]: any): number); + + for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) { + const suspenseNodeId = operations[i++]; + const hasUniqueSuspenders = 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`, + ); + } } - i += 2 + changesLength * 2; break; } 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 7e74f21eef08c..7ad31524968e8 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.css @@ -19,6 +19,10 @@ overflow: hidden; } +.SuspenseRectsScaledRect[data-visible='false'] > .SuspenseRectsBoundaryChildren { + overflow: initial; +} + .SuspenseRectsRect { box-shadow: var(--elevation-4); pointer-events: all; @@ -31,6 +35,11 @@ outline-color: var(--color-background-selected); } +.SuspenseRectsScaledRect[data-visible='false'] { + pointer-events: none; + outline-width: 0; +} + /* 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 39c6f1c492517..5dac93a182c1c 100644 --- a/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js +++ b/packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js @@ -34,10 +34,12 @@ import { function ScaledRect({ className, rect, + visible, ...props }: { className: string, rect: Rect, + visible: boolean, ... }): React$Node { const viewBox = useContext(ViewBox); @@ -50,6 +52,7 @@ function ScaledRect({
+ - {suspense.rects !== null && + {visible && + suspense.rects !== null && suspense.rects.map((rect, index) => { return ( (null); const resizeTreeRef = useRef(null); const resizeTreeListRef = useRef(null); @@ -290,23 +295,31 @@ function SuspenseTab(_: {}) { return (
- - + )} + {treeListDisabled ? null : ( +