Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ type SuspenseNode = {
nextSibling: null | SuspenseNode,
rects: null | Array<Rect>, // The bounding rects of content children.
suspendedBy: Map<ReactIOInfo, Set<DevToolsInstance>>, // Tracks which data we're suspended by and the children that suspend it.
environments: Map<string, number>, // 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,
Expand Down Expand Up @@ -327,6 +328,7 @@ function createSuspenseNode(
nextSibling: null,
rects: null,
suspendedBy: new Map(),
environments: new Map(),
hasUniqueSuspenders: false,
hasUnknownSuspenders: false,
});
Expand Down Expand Up @@ -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);
});
});
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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 &&
Expand All @@ -2948,6 +2994,9 @@ export function attach(
}
}
}
if (changedEnvironment) {
recordSuspenseSuspenders(suspenseNode);
}
}
}

Expand Down
21 changes: 15 additions & 6 deletions packages/react-devtools-shared/src/devtools/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -1777,8 +1787,6 @@ export default class Store extends EventEmitter<{
break;
}

i += 2;

if (__DEBUG__) {
const previousHasUniqueSuspenders = suspense.hasUniqueSuspenders;
debug(
Expand All @@ -1788,6 +1796,7 @@ export default class Store extends EventEmitter<{
}

suspense.hasUniqueSuspenders = hasUniqueSuspenders;
// TODO: Recompute the environment names.
}

hasSuspenseTreeChanged = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
overflow: hidden;
}

.SuspenseRectsScaledRect[data-visible='false'] > .SuspenseRectsBoundaryChildren {
overflow: initial;
}

.SuspenseRectsRect {
box-shadow: var(--elevation-4);
pointer-events: all;
Expand 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ import {
function ScaledRect({
className,
rect,
visible,
...props
}: {
className: string,
rect: Rect,
visible: boolean,
...
}): React$Node {
const viewBox = useContext(ViewBox);
Expand All @@ -50,6 +52,7 @@ function ScaledRect({
<div
{...props}
className={styles.SuspenseRectsScaledRect + ' ' + className}
data-visible={visible}
style={{
width,
height,
Expand All @@ -68,6 +71,7 @@ function SuspenseRects({
const store = useContext(StoreContext);
const treeDispatch = useContext(TreeDispatcherContext);
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
const {uniqueSuspendersOnly} = useContext(SuspenseTreeStateContext);

const {inspectedElementID} = useContext(TreeStateContext);

Expand All @@ -79,6 +83,7 @@ function SuspenseRects({
// getSuspenseByID will have already warned
return null;
}
const visible = suspense.hasUniqueSuspenders || !uniqueSuspendersOnly;

function handleClick(event: SyntheticMouseEvent) {
if (event.defaultPrevented) {
Expand Down Expand Up @@ -117,9 +122,13 @@ function SuspenseRects({
const boundingBox = getBoundingBox(suspense.rects);

return (
<ScaledRect rect={boundingBox} className={styles.SuspenseRectsBoundary}>
<ScaledRect
rect={boundingBox}
className={styles.SuspenseRectsBoundary}
visible={visible}>
<ViewBox.Provider value={boundingBox}>
{suspense.rects !== null &&
{visible &&
suspense.rects !== null &&
suspense.rects.map((rect, index) => {
return (
<ScaledRect
Expand Down Expand Up @@ -245,6 +254,7 @@ function SuspenseRectsContainer(): React$Node {
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
const {roots} = useContext(SuspenseTreeStateContext);

// TODO: bbox does not consider uniqueSuspendersOnly filter
const boundingBox = getDocumentBoundingRect(store, roots);

const boundingBoxWidth = boundingBox.width;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ function SuspenseTab(_: {}) {
initLayoutState,
);

// If there are no named Activity boundaries, we don't have any tree list and we should hide
// both the panel and the button to toggle it. Since we currently don't support it yet, it's
// always disabled.
const treeListDisabled = true;

const wrapperTreeRef = useRef<null | HTMLElement>(null);
const resizeTreeRef = useRef<null | HTMLElement>(null);
const resizeTreeListRef = useRef<null | HTMLElement>(null);
Expand Down Expand Up @@ -290,23 +295,31 @@ function SuspenseTab(_: {}) {
return (
<div className={styles.SuspenseTab} ref={wrapperTreeRef}>
<div className={styles.TreeWrapper} ref={resizeTreeRef}>
<div
className={styles.TreeList}
hidden={treeListHidden}
ref={resizeTreeListRef}>
<SuspenseTreeList />
</div>
<div className={styles.ResizeBarWrapper} hidden={treeListHidden}>
{treeListDisabled ? null : (
<div
onPointerDown={onResizeStart}
onPointerMove={onResizeTreeList}
onPointerUp={onResizeEnd}
className={styles.ResizeBar}
/>
</div>
className={styles.TreeList}
hidden={treeListHidden}
ref={resizeTreeListRef}>
<SuspenseTreeList />
</div>
)}
{treeListDisabled ? null : (
<div className={styles.ResizeBarWrapper} hidden={treeListHidden}>
<div
onPointerDown={onResizeStart}
onPointerMove={onResizeTreeList}
onPointerUp={onResizeEnd}
className={styles.ResizeBar}
/>
</div>
)}
<div className={styles.TreeView}>
<div className={styles.SuspenseTreeViewHeader}>
<ToggleTreeList dispatch={dispatch} state={state} />
{treeListDisabled ? (
<div />
) : (
<ToggleTreeList dispatch={dispatch} state={state} />
)}
<div className={styles.SuspenseTreeViewHeaderMain}>
<div className={styles.SuspenseTimeline}>
<SuspenseTimeline />
Expand Down
18 changes: 12 additions & 6 deletions packages/react-devtools-shared/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,12 +426,18 @@ export function printOperationsArray(operations: Array<number>) {
break;
}
case SUSPENSE_TREE_OPERATION_SUSPENDERS: {
const changeLength = operations[i + 1];
i += 2;
const changes = operations.slice(i, i + changeLength * 2);
i += changeLength;

logs.push(`Suspense node suspender changes ${changes.join(',')}`);
i++;
const changeLength = ((operations[i++]: any): number);

for (let changeIndex = 0; changeIndex < changeLength; changeIndex++) {
const id = operations[i++];
const hasUniqueSuspenders = operations[i++] === 1;
const environmentNamesLength = operations[i++];
i += environmentNamesLength;
logs.push(
`Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} with ${String(environmentNamesLength)} environments`,
);
}

break;
}
Expand Down
Loading
Loading