Skip to content

Commit 94788ae

Browse files
committed
[DevTools] Name root "Transition" when focusing on Activity
1 parent a782ec9 commit 94788ae

File tree

6 files changed

+201
-45
lines changed

6 files changed

+201
-45
lines changed

packages/react-devtools-shared/src/devtools/store.js

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ export default class Store extends EventEmitter<{
189189
{errorCount: number, warningCount: number},
190190
> = new Map();
191191

192+
_focusedTransition: 0 | Element['id'] = 0;
193+
192194
// At least one of the injected renderers contains (DEV only) owner metadata.
193195
_hasOwnerMetadata: boolean = false;
194196

@@ -372,11 +374,12 @@ export default class Store extends EventEmitter<{
372374
if (map.size !== expectedSize) {
373375
this._throwAndEmitError(
374376
Error(
375-
`Expected ${mapName} to contain ${expectedSize} items, but it contains ${
376-
map.size
377-
} items\n\n${inspect(map, {
378-
depth: 20,
379-
})}`,
377+
`Expected ${mapName} to contain ${expectedSize} items, but it contains ${map.size} items\n\n${inspect(
378+
map,
379+
{
380+
depth: 20,
381+
},
382+
)}`,
380383
),
381384
);
382385
}
@@ -935,10 +938,9 @@ export default class Store extends EventEmitter<{
935938
}
936939

937940
/**
938-
* @param rootID
939941
* @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
940942
*/
941-
getSuspendableDocumentOrderSuspense(
943+
getSuspendableDocumentOrderSuspenseInitialPaint(
942944
uniqueSuspendersOnly: boolean,
943945
): Array<SuspenseTimelineStep> {
944946
const target: Array<SuspenseTimelineStep> = [];
@@ -990,6 +992,76 @@ export default class Store extends EventEmitter<{
990992
return target;
991993
}
992994

995+
_pushSuspenseChildrenInDocumentOrder(
996+
children: Array<Element['id']>,
997+
target: Array<SuspenseNode['id']>,
998+
): void {
999+
for (let i = 0; i < children.length; i++) {
1000+
const childID = children[i];
1001+
const suspense = this.getSuspenseByID(childID);
1002+
if (suspense !== null) {
1003+
target.push(suspense.id);
1004+
} else {
1005+
const childElement = this.getElementByID(childID);
1006+
if (childElement !== null) {
1007+
this._pushSuspenseChildrenInDocumentOrder(
1008+
childElement.children,
1009+
target,
1010+
);
1011+
}
1012+
}
1013+
}
1014+
}
1015+
1016+
getSuspenseChildren(id: Element['id']): Array<SuspenseNode['id']> {
1017+
const transitionChildren: Array<SuspenseNode['id']> = [];
1018+
1019+
const root = this._idToElement.get(id);
1020+
if (root === undefined) {
1021+
return transitionChildren;
1022+
}
1023+
1024+
this._pushSuspenseChildrenInDocumentOrder(
1025+
root.children,
1026+
transitionChildren,
1027+
);
1028+
1029+
return transitionChildren;
1030+
}
1031+
1032+
/**
1033+
* @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
1034+
*/
1035+
getSuspendableDocumentOrderSuspenseTransition(
1036+
uniqueSuspendersOnly: boolean,
1037+
): Array<SuspenseTimelineStep> {
1038+
const target: Array<SuspenseTimelineStep> = [];
1039+
const focusedTransitionID = this._focusedTransition;
1040+
if (focusedTransitionID === null) {
1041+
return target;
1042+
}
1043+
1044+
target.push({
1045+
id: focusedTransitionID,
1046+
// TODO: Get environment for Activity
1047+
environment: null,
1048+
endTime: 0,
1049+
});
1050+
1051+
const transitionChildren = this.getSuspenseChildren(focusedTransitionID);
1052+
1053+
this.pushTimelineStepsInDocumentOrder(
1054+
transitionChildren,
1055+
target,
1056+
uniqueSuspendersOnly,
1057+
// TODO: Get environment for Activity
1058+
[],
1059+
0, // Don't pass a minimum end time at the root. The root is always first so doesn't matter.
1060+
);
1061+
1062+
return target;
1063+
}
1064+
9931065
pushTimelineStepsInDocumentOrder(
9941066
children: Array<SuspenseNode['id']>,
9951067
target: Array<SuspenseTimelineStep>,
@@ -1045,7 +1117,14 @@ export default class Store extends EventEmitter<{
10451117
uniqueSuspendersOnly: boolean,
10461118
): $ReadOnlyArray<SuspenseTimelineStep> {
10471119
const timeline =
1048-
this.getSuspendableDocumentOrderSuspense(uniqueSuspendersOnly);
1120+
this._focusedTransition === 0
1121+
? this.getSuspendableDocumentOrderSuspenseInitialPaint(
1122+
uniqueSuspendersOnly,
1123+
)
1124+
: this.getSuspendableDocumentOrderSuspenseTransition(
1125+
uniqueSuspendersOnly,
1126+
);
1127+
10491128
if (timeline.length === 0) {
10501129
return timeline;
10511130
}
@@ -1271,7 +1350,7 @@ export default class Store extends EventEmitter<{
12711350
const removedElementIDs: Map<number, number> = new Map();
12721351
const removedSuspenseIDs: Map<SuspenseNode['id'], SuspenseNode['id']> =
12731352
new Map();
1274-
let nextActivitySliceID = null;
1353+
let nextActivitySliceID: Element['id'] | null = null;
12751354

12761355
let i = 2;
12771356

@@ -1991,7 +2070,9 @@ export default class Store extends EventEmitter<{
19912070
const previousHasUniqueSuspenders = suspense.hasUniqueSuspenders;
19922071
debug(
19932072
'Suspender changes',
1994-
`Suspense node ${id} unique suspenders set to ${String(hasUniqueSuspenders)} (was ${String(previousHasUniqueSuspenders)})`,
2073+
`Suspense node ${id} unique suspenders set to ${String(
2074+
hasUniqueSuspenders,
2075+
)} (was ${String(previousHasUniqueSuspenders)})`,
19952076
);
19962077
}
19972078

@@ -2146,6 +2227,10 @@ export default class Store extends EventEmitter<{
21462227
}
21472228
}
21482229

2230+
if (nextActivitySliceID !== null) {
2231+
this._focusedTransition = nextActivitySliceID;
2232+
}
2233+
21492234
this.emit('mutated', [
21502235
addedElementIDs,
21512236
removedElementIDs,

packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseBreadcrumbs.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import typeof {SyntheticMouseEvent} from 'react-dom-bindings/src/events/Syntheti
1212

1313
import * as React from 'react';
1414
import {useContext} from 'react';
15-
import {TreeDispatcherContext} from '../Components/TreeContext';
15+
import {
16+
TreeDispatcherContext,
17+
TreeStateContext,
18+
} from '../Components/TreeContext';
1619
import {StoreContext} from '../context';
1720
import {useHighlightHostInstance} from '../hooks';
1821
import styles from './SuspenseBreadcrumbs.css';
@@ -23,6 +26,7 @@ import {
2326

2427
export default function SuspenseBreadcrumbs(): React$Node {
2528
const store = useContext(StoreContext);
29+
const {activityID} = useContext(TreeStateContext);
2630
const treeDispatch = useContext(TreeDispatcherContext);
2731
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
2832
const {selectedSuspenseID, lineage, roots} = useContext(
@@ -42,18 +46,21 @@ export default function SuspenseBreadcrumbs(): React$Node {
4246
<ol className={styles.SuspenseBreadcrumbsList}>
4347
{lineage === null ? null : lineage.length === 0 ? (
4448
// We selected the root. This means that we're currently viewing the Transition
45-
// that rendered the whole screen. In laymans terms this is really "Initial Paint".
46-
// TODO: Once we add subtree selection, then the equivalent should be called
49+
// that rendered the whole screen. In laymans terms this is really "Initial Paint" .
50+
// When we're looking at a subtree selection, then the equivalent is a
4751
// "Transition" since in that case it's really about a Transition within the page.
4852
roots.length > 0 ? (
4953
<li
5054
className={styles.SuspenseBreadcrumbsListItem}
5155
aria-current="true">
5256
<button
5357
className={styles.SuspenseBreadcrumbsButton}
54-
onClick={handleClick.bind(null, roots[0])}
58+
onClick={handleClick.bind(
59+
null,
60+
activityID === null ? roots[0] : activityID,
61+
)}
5562
type="button">
56-
Initial Paint
63+
{activityID === null ? 'Initial Paint' : 'Transition'}
5764
</button>
5865
</li>
5966
) : null

packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseRects.js

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import type Store from 'react-devtools-shared/src/devtools/store';
1111
import type {
12+
Element,
1213
SuspenseNode,
1314
Rect,
1415
} from 'react-devtools-shared/src/frontend/types';
@@ -18,7 +19,7 @@ import typeof {
1819
} from 'react-dom-bindings/src/events/SyntheticEvent';
1920

2021
import * as React from 'react';
21-
import {createContext, useContext, useLayoutEffect} from 'react';
22+
import {createContext, useContext, useLayoutEffect, useMemo} from 'react';
2223
import {
2324
TreeDispatcherContext,
2425
TreeStateContext,
@@ -426,6 +427,30 @@ function SuspenseRectsRoot({rootID}: {rootID: SuspenseNode['id']}): React$Node {
426427
});
427428
}
428429

430+
function SuspenseRectsInitialPaint(): React$Node {
431+
const {roots} = useContext(SuspenseTreeStateContext);
432+
return roots.map(rootID => {
433+
return <SuspenseRectsRoot key={rootID} rootID={rootID} />;
434+
});
435+
}
436+
437+
function SuspenseRectsTransition({id}: {id: Element['id']}): React$Node {
438+
const store = useContext(StoreContext);
439+
const children = useMemo(() => {
440+
return store.getSuspenseChildren(id);
441+
}, [id, store]);
442+
443+
return children.map(suspenseID => {
444+
return (
445+
<SuspenseRects
446+
key={suspenseID}
447+
suspenseID={suspenseID}
448+
parentRects={null}
449+
/>
450+
);
451+
});
452+
}
453+
429454
const ViewBox = createContext<Rect>((null: any));
430455

431456
function SuspenseRectsContainer({
@@ -434,14 +459,25 @@ function SuspenseRectsContainer({
434459
scaleRef: {current: number},
435460
}): React$Node {
436461
const store = useContext(StoreContext);
437-
const {inspectedElementID} = useContext(TreeStateContext);
462+
const {activityID, inspectedElementID} = useContext(TreeStateContext);
438463
const treeDispatch = useContext(TreeDispatcherContext);
439464
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
440465
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
441466
const {roots, timeline, hoveredTimelineIndex, uniqueSuspendersOnly} =
442467
useContext(SuspenseTreeStateContext);
443468

444-
// TODO: bbox does not consider uniqueSuspendersOnly filter
469+
const activityChildren: $ReadOnlyArray<SuspenseNode['id']> | null =
470+
useMemo(() => {
471+
if (activityID === null) {
472+
return null;
473+
}
474+
return store.getSuspenseChildren(activityID);
475+
}, [activityID, store]);
476+
const transitionChildren =
477+
activityChildren === null ? roots : activityChildren;
478+
479+
// We're using the bounding box of the entire document to anchor the Transition
480+
// in the actual document.
445481
const boundingBox = getDocumentBoundingRect(store, roots);
446482

447483
const boundingBoxWidth = boundingBox.width;
@@ -456,14 +492,18 @@ function SuspenseRectsContainer({
456492
// Already clicked on an inner rect
457493
return;
458494
}
459-
if (roots.length === 0) {
495+
if (transitionChildren.length === 0) {
460496
// Nothing to select
461497
return;
462498
}
463499
const arbitraryRootID = roots[0];
500+
const transitionRoot = activityID === null ? arbitraryRootID : activityID;
464501

465502
event.preventDefault();
466-
treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: arbitraryRootID});
503+
treeDispatch({
504+
type: 'SELECT_ELEMENT_BY_ID',
505+
payload: transitionRoot,
506+
});
467507
suspenseTreeDispatch({
468508
type: 'SET_SUSPENSE_LINEAGE',
469509
payload: arbitraryRootID,
@@ -483,7 +523,8 @@ function SuspenseRectsContainer({
483523
}
484524

485525
const isRootSelected = roots.includes(inspectedElementID);
486-
const isRootHovered = hoveredTimelineIndex === 0;
526+
// When we're focusing a Transition, the first timeline step will not be a root.
527+
const isRootHovered = activityID === null && hoveredTimelineIndex === 0;
487528

488529
let hasRootSuspenders = false;
489530
if (!uniqueSuspendersOnly) {
@@ -536,7 +577,13 @@ function SuspenseRectsContainer({
536577
<div
537578
className={
538579
styles.SuspenseRectsContainer +
539-
(hasRootSuspenders ? ' ' + styles.SuspenseRectsRoot : '') +
580+
(hasRootSuspenders &&
581+
// We don't want to draw attention to the root if we're looking at a Transition.
582+
// TODO: Draw bounding rect of Transition and check if the Transition
583+
// has unique suspenders.
584+
activityID === null
585+
? ' ' + styles.SuspenseRectsRoot
586+
: '') +
540587
(isRootSelected ? ' ' + styles.SuspenseRectsRootOutline : '') +
541588
' ' +
542589
getClassNameForEnvironment(rootEnvironment)
@@ -548,9 +595,11 @@ function SuspenseRectsContainer({
548595
<div
549596
className={styles.SuspenseRectsViewBox}
550597
style={{aspectRatio, width}}>
551-
{roots.map(rootID => {
552-
return <SuspenseRectsRoot key={rootID} rootID={rootID} />;
553-
})}
598+
{activityID === null ? (
599+
<SuspenseRectsInitialPaint />
600+
) : (
601+
<SuspenseRectsTransition id={activityID} />
602+
)}
554603
{selectedBoundingBox !== null ? (
555604
<ScaledRect
556605
className={

packages/react-devtools-shared/src/devtools/views/SuspenseTab/SuspenseScrubber.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import type {SuspenseTimelineStep} from 'react-devtools-shared/src/frontend/type
1212
import typeof {SyntheticEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
1313

1414
import * as React from 'react';
15-
import {useRef} from 'react';
15+
import {useContext, useRef} from 'react';
16+
import {ElementTypeRoot} from 'react-devtools-shared/src/frontend/types';
1617

1718
import styles from './SuspenseScrubber.css';
1819

1920
import {getClassNameForEnvironment} from './SuspenseEnvironmentColors.js';
2021

2122
import Tooltip from '../Components/reach-ui/tooltip';
23+
import {StoreContext} from '../context';
2224

2325
export default function SuspenseScrubber({
2426
min,
@@ -43,6 +45,7 @@ export default function SuspenseScrubber({
4345
onHoverSegment: (index: number) => void,
4446
onHoverLeave: () => void,
4547
}): React$Node {
48+
const store = useContext(StoreContext);
4649
const inputRef = useRef();
4750
function handleChange(event: SyntheticEvent) {
4851
const newValue = +event.currentTarget.value;
@@ -60,12 +63,16 @@ export default function SuspenseScrubber({
6063
}
6164
const steps = [];
6265
for (let index = min; index <= max; index++) {
63-
const environment = timeline[index].environment;
66+
const step = timeline[index];
67+
const environment = step.environment;
68+
const element = store.getElementByID(step.id);
6469
const label =
6570
index === min
6671
? // The first step in the timeline is always a Transition (Initial Paint).
67-
'Initial Paint' +
68-
(environment === null ? '' : ' (' + environment + ')')
72+
element === null || element.type === ElementTypeRoot
73+
? 'Initial Paint'
74+
: 'Transition' +
75+
(environment === null ? '' : ' (' + environment + ')')
6976
: // TODO: Consider adding the name of this specific boundary if this step has only one.
7077
environment === null
7178
? 'Suspense'

0 commit comments

Comments
 (0)