Skip to content

Commit de9e00c

Browse files
committed
 [DevTools] flatten all Suspense boundaries into a single timeline
1 parent 720bb13 commit de9e00c

File tree

8 files changed

+68
-227
lines changed

8 files changed

+68
-227
lines changed

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -974,12 +974,8 @@ describe('Store', () => {
974974
<Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
975975
`);
976976

977-
const rendererID = getRendererID();
978-
const rootID = store.getRootIDForElement(store.getElementIDAtIndex(0));
979977
await actAsync(() => {
980978
agent.overrideSuspenseMilestone({
981-
rendererID,
982-
rootID,
983979
suspendedSet: [
984980
store.getElementIDAtIndex(4),
985981
store.getElementIDAtIndex(8),
@@ -1009,8 +1005,6 @@ describe('Store', () => {
10091005

10101006
await actAsync(() => {
10111007
agent.overrideSuspenseMilestone({
1012-
rendererID,
1013-
rootID,
10141008
suspendedSet: [],
10151009
});
10161010
});

packages/react-devtools-shared/src/backend/agent.js

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,6 @@ type OverrideSuspenseParams = {
131131
};
132132

133133
type OverrideSuspenseMilestoneParams = {
134-
rendererID: number,
135-
rootID: number,
136134
suspendedSet: Array<number>,
137135
};
138136

@@ -567,17 +565,13 @@ export default class Agent extends EventEmitter<{
567565
};
568566

569567
overrideSuspenseMilestone: OverrideSuspenseMilestoneParams => void = ({
570-
rendererID,
571-
rootID,
572568
suspendedSet,
573569
}) => {
574-
const renderer = this._rendererInterfaces[rendererID];
575-
if (renderer == null) {
576-
console.warn(
577-
`Invalid renderer id "${rendererID}" to override suspense milestone`,
578-
);
579-
} else {
580-
renderer.overrideSuspenseMilestone(rootID, suspendedSet);
570+
for (const rendererID in this._rendererInterfaces) {
571+
const renderer = ((this._rendererInterfaces[
572+
(rendererID: any)
573+
]: any): RendererInterface);
574+
renderer.overrideSuspenseMilestone(suspendedSet);
581575
}
582576
};
583577

packages/react-devtools-shared/src/backend/fiber/renderer.js

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7756,10 +7756,7 @@ export function attach(
77567756
* @param rootID The root that contains this milestone
77577757
* @param suspendedSet List of IDs of SuspenseComponent Fibers
77587758
*/
7759-
function overrideSuspenseMilestone(
7760-
rootID: FiberInstance['id'],
7761-
suspendedSet: Array<FiberInstance['id']>,
7762-
) {
7759+
function overrideSuspenseMilestone(suspendedSet: Array<FiberInstance['id']>) {
77637760
if (
77647761
typeof setSuspenseHandler !== 'function' ||
77657762
typeof scheduleUpdate !== 'function'
@@ -7769,7 +7766,6 @@ export function attach(
77697766
);
77707767
}
77717768
7772-
// TODO: Allow overriding the timeline for the specified root.
77737769
forceFallbackForFibers.forEach(fiber => {
77747770
scheduleUpdate(fiber);
77757771
});
@@ -7778,9 +7774,7 @@ export function attach(
77787774
for (let i = 0; i < suspendedSet.length; ++i) {
77797775
const instance = idToDevToolsInstanceMap.get(suspendedSet[i]);
77807776
if (instance === undefined) {
7781-
console.warn(
7782-
`Could not suspend ID '${suspendedSet[i]}' since the instance can't be found.`,
7783-
);
7777+
// this is an ID from a different root or even renderer.
77847778
continue;
77857779
}
77867780

packages/react-devtools-shared/src/backend/types.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -437,10 +437,7 @@ export type RendererInterface = {
437437
onErrorOrWarning?: OnErrorOrWarning,
438438
overrideError: (id: number, forceError: boolean) => void,
439439
overrideSuspense: (id: number, forceFallback: boolean) => void,
440-
overrideSuspenseMilestone: (
441-
rootID: number,
442-
suspendedSet: Array<number>,
443-
) => void,
440+
overrideSuspenseMilestone: (suspendedSet: Array<number>) => void,
444441
overrideValueAtPath: (
445442
type: Type,
446443
id: number,

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,6 @@ type OverrideSuspense = {
141141
};
142142

143143
type OverrideSuspenseMilestone = {
144-
rendererID: number,
145-
rootID: number,
146144
suspendedSet: Array<number>,
147145
};
148146

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

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -885,38 +885,39 @@ export default class Store extends EventEmitter<{
885885
* @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
886886
*/
887887
getSuspendableDocumentOrderSuspense(
888-
rootID: Element['id'] | void,
889888
uniqueSuspendersOnly: boolean,
890889
): $ReadOnlyArray<SuspenseNode['id']> {
891-
if (rootID === undefined) {
892-
return [];
893-
}
894-
const root = this.getElementByID(rootID);
895-
if (root === null) {
896-
return [];
897-
}
898-
if (!this.supportsTogglingSuspense(rootID)) {
899-
return [];
900-
}
901890
const list: SuspenseNode['id'][] = [];
902-
const suspense = this.getSuspenseByID(rootID);
903-
if (suspense !== null) {
904-
const stack = [suspense];
905-
while (stack.length > 0) {
906-
const current = stack.pop();
907-
if (current === undefined) {
908-
continue;
909-
}
910-
// Include the root even if we won't show it suspended (because that's just blank).
911-
// You should be able to see what suspended the shell.
912-
if (!uniqueSuspendersOnly || current.hasUniqueSuspenders) {
913-
list.push(current.id);
914-
}
915-
// Add children in reverse order to maintain document order
916-
for (let j = current.children.length - 1; j >= 0; j--) {
917-
const childSuspense = this.getSuspenseByID(current.children[j]);
918-
if (childSuspense !== null) {
919-
stack.push(childSuspense);
891+
// Arbitrarily pick the order in which roots were committed as document-order.
892+
for (let i = 0; i < this._roots.length; i++) {
893+
const rootID = this._roots[i];
894+
const root = this.getElementByID(rootID);
895+
896+
if (root === null) {
897+
return [];
898+
}
899+
if (!this.supportsTogglingSuspense(rootID)) {
900+
return [];
901+
}
902+
const suspense = this.getSuspenseByID(rootID);
903+
if (suspense !== null) {
904+
const stack = [suspense];
905+
while (stack.length > 0) {
906+
const current = stack.pop();
907+
if (current === undefined) {
908+
continue;
909+
}
910+
// Include the root even if we won't show it suspended (because that's just blank).
911+
// You should be able to see what suspended the shell.
912+
if (!uniqueSuspendersOnly || current.hasUniqueSuspenders) {
913+
list.push(current.id);
914+
}
915+
// Add children in reverse order to maintain document order
916+
for (let j = current.children.length - 1; j >= 0; j--) {
917+
const childSuspense = this.getSuspenseByID(current.children[j]);
918+
if (childSuspense !== null) {
919+
stack.push(childSuspense);
920+
}
920921
}
921922
}
922923
}

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

Lines changed: 8 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,16 @@ function SuspenseTimelineInput() {
3131
const {highlightHostInstance, clearHighlightHostInstance} =
3232
useHighlightHostInstance();
3333

34-
const {
35-
selectedRootID: rootID,
36-
timeline,
37-
timelineIndex,
38-
uniqueSuspendersOnly,
39-
} = useContext(SuspenseTreeStateContext);
34+
const {timeline, timelineIndex, uniqueSuspendersOnly} = useContext(
35+
SuspenseTreeStateContext,
36+
);
4037

4138
function handleToggleUniqueSuspenders(event: SyntheticEvent) {
4239
const nextUniqueSuspendersOnly = (event.currentTarget as HTMLInputElement)
4340
.checked;
44-
const nextTimeline =
45-
rootID === null
46-
? []
47-
: // TODO: Handle different timeline modes (e.g. random order)
48-
store.getSuspendableDocumentOrderSuspense(
49-
rootID,
50-
nextUniqueSuspendersOnly,
51-
);
41+
const nextTimeline = store.getSuspendableDocumentOrderSuspense(
42+
nextUniqueSuspendersOnly,
43+
);
5244
suspenseTreeDispatch({
5345
type: 'SET_SUSPENSE_TIMELINE',
5446
payload: [nextTimeline, null, nextUniqueSuspendersOnly],
@@ -81,24 +73,10 @@ function SuspenseTimelineInput() {
8173
const min = 0;
8274
const max = timeline.length > 0 ? timeline.length - 1 : 0;
8375

84-
if (rootID === null) {
85-
return (
86-
<div className={styles.SuspenseTimelineInput}>No root selected.</div>
87-
);
88-
}
89-
90-
if (!store.supportsTogglingSuspense(rootID)) {
91-
return (
92-
<div className={styles.SuspenseTimelineInput}>
93-
Can't step through Suspense in production apps.
94-
</div>
95-
);
96-
}
97-
9876
if (timeline.length === 0) {
9977
return (
10078
<div className={styles.SuspenseTimelineInput}>
101-
Root contains no Suspense nodes.
79+
Timeline contains no suspendable boundaries.
10280
</div>
10381
);
10482
}
@@ -117,23 +95,10 @@ function SuspenseTimelineInput() {
11795
}
11896

11997
function handleChange(event: SyntheticEvent) {
120-
if (rootID === null) {
121-
return;
122-
}
123-
const rendererID = store.getRendererIDForElement(rootID);
124-
if (rendererID === null) {
125-
console.error(
126-
`No renderer ID found for root element ${rootID} in suspense timeline.`,
127-
);
128-
return;
129-
}
130-
13198
const pendingTimelineIndex = +event.currentTarget.value;
13299
const suspendedSet = timeline.slice(pendingTimelineIndex);
133100

134101
bridge.send('overrideSuspenseMilestone', {
135-
rendererID,
136-
rootID,
137102
suspendedSet,
138103
});
139104

@@ -202,54 +167,9 @@ function SuspenseTimelineInput() {
202167
}
203168

204169
export default function SuspenseTimeline(): React$Node {
205-
const store = useContext(StoreContext);
206-
const {roots, selectedRootID, uniqueSuspendersOnly} = useContext(
207-
SuspenseTreeStateContext,
208-
);
209-
const treeDispatch = useContext(TreeDispatcherContext);
210-
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
211-
212-
function handleChange(event: SyntheticEvent) {
213-
const newRootID = +event.currentTarget.value;
214-
// TODO: scrollIntoView both suspense rects and host instance.
215-
const nextTimeline = store.getSuspendableDocumentOrderSuspense(
216-
newRootID,
217-
uniqueSuspendersOnly,
218-
);
219-
suspenseTreeDispatch({
220-
type: 'SET_SUSPENSE_TIMELINE',
221-
payload: [nextTimeline, newRootID, uniqueSuspendersOnly],
222-
});
223-
if (nextTimeline.length > 0) {
224-
const milestone = nextTimeline[nextTimeline.length - 1];
225-
treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: milestone});
226-
}
227-
}
228-
229170
return (
230171
<div className={styles.SuspenseTimelineContainer}>
231-
<SuspenseTimelineInput key={selectedRootID} />
232-
{roots.length > 0 && (
233-
<select
234-
aria-label="Select Suspense Root"
235-
className={styles.SuspenseTimelineRootSwitcher}
236-
onChange={handleChange}
237-
value={selectedRootID === null ? -1 : selectedRootID}>
238-
<option disabled={true} value={-1}>
239-
----
240-
</option>
241-
{roots.map(rootID => {
242-
// TODO: Use name
243-
const name = '#' + rootID;
244-
// TODO: Highlight host on hover
245-
return (
246-
<option key={rootID} value={rootID}>
247-
{name}
248-
</option>
249-
);
250-
})}
251-
</select>
252-
)}
172+
<SuspenseTimelineInput />
253173
</div>
254174
);
255175
}

0 commit comments

Comments
 (0)