Skip to content

Commit 3cc5645

Browse files
authored
SuspenseList support in DevTools (#17145)
* SuspenseList support in DevTools This adds SuspenseList tags to DevTools so that the name properly shows up. It also switches to use the tag instead of Symbol type for Suspense components. We shouldn't rely on the type for any built-ins since that field will disappear from the fibers. How the Fibers get created is an implementation detail that can change e.g. with a compiler or if we use instanceof checks that are faster than symbol comparisons. * Add SuspenseList test to shell app
1 parent 68fb580 commit 3cc5645

File tree

5 files changed

+95
-22
lines changed

5 files changed

+95
-22
lines changed

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,25 @@ exports[`Store collapseNodesByDefault:false should display Suspense nodes proper
1616
<Component key="Inside">
1717
`;
1818

19+
exports[`Store collapseNodesByDefault:false should display a partially rendered SuspenseList: 1: loading 1`] = `
20+
[root]
21+
▾ <Wrapper>
22+
▾ <SuspenseList>
23+
<Component key="A">
24+
▾ <Suspense>
25+
<Loading>
26+
`;
27+
28+
exports[`Store collapseNodesByDefault:false should display a partially rendered SuspenseList: 2: resolved 1`] = `
29+
[root]
30+
▾ <Wrapper>
31+
▾ <SuspenseList>
32+
<Component key="A">
33+
▾ <Suspense>
34+
<Component key="B">
35+
<Component key="C">
36+
`;
37+
1938
exports[`Store collapseNodesByDefault:false should filter DOM nodes from the store tree: 1: mount 1`] = `
2039
[root]
2140
▾ <Grandparent>

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,39 @@ describe('Store', () => {
342342
expect(store).toMatchSnapshot('13: third child is suspended');
343343
});
344344

345+
it('should display a partially rendered SuspenseList', () => {
346+
const Loading = () => <div>Loading...</div>;
347+
const SuspendingComponent = () => {
348+
throw new Promise(() => {});
349+
};
350+
const Component = () => {
351+
return <div>Hello</div>;
352+
};
353+
const Wrapper = ({shouldSuspense}) => (
354+
<React.Fragment>
355+
<React.SuspenseList revealOrder="forwards" tail="collapsed">
356+
<Component key="A" />
357+
<React.Suspense fallback={<Loading />}>
358+
{shouldSuspense ? <SuspendingComponent /> : <Component key="B" />}
359+
</React.Suspense>
360+
<Component key="C" />
361+
</React.SuspenseList>
362+
</React.Fragment>
363+
);
364+
365+
const container = document.createElement('div');
366+
const root = ReactDOM.createRoot(container);
367+
act(() => {
368+
root.render(<Wrapper shouldSuspense={true} />);
369+
});
370+
expect(store).toMatchSnapshot('1: loading');
371+
372+
act(() => {
373+
root.render(<Wrapper shouldSuspense={false} />);
374+
});
375+
expect(store).toMatchSnapshot('2: resolved');
376+
});
377+
345378
it('should support collapsing parts of the tree', () => {
346379
const Grandparent = ({count}) => (
347380
<React.Fragment>

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

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
ElementTypeProfiler,
2424
ElementTypeRoot,
2525
ElementTypeSuspense,
26+
ElementTypeSuspenseList,
2627
} from 'react-devtools-shared/src/types';
2728
import {
2829
getDisplayName,
@@ -91,9 +92,6 @@ type ReactSymbolsType = {
9192
PROFILER_SYMBOL_STRING: string,
9293
STRICT_MODE_NUMBER: number,
9394
STRICT_MODE_SYMBOL_STRING: string,
94-
SUSPENSE_NUMBER: number,
95-
SUSPENSE_SYMBOL_STRING: string,
96-
DEPRECATED_PLACEHOLDER_SYMBOL_STRING: string,
9795
SCOPE_NUMBER: number,
9896
SCOPE_SYMBOL_STRING: string,
9997
};
@@ -129,6 +127,7 @@ type ReactTypeOfWorkType = {|
129127
Profiler: number,
130128
SimpleMemoComponent: number,
131129
SuspenseComponent: number,
130+
SuspenseListComponent: number,
132131
YieldComponent: number,
133132
|};
134133

@@ -170,9 +169,6 @@ export function getInternalReactConstants(
170169
PROFILER_SYMBOL_STRING: 'Symbol(react.profiler)',
171170
STRICT_MODE_NUMBER: 0xeacc,
172171
STRICT_MODE_SYMBOL_STRING: 'Symbol(react.strict_mode)',
173-
SUSPENSE_NUMBER: 0xead1,
174-
SUSPENSE_SYMBOL_STRING: 'Symbol(react.suspense)',
175-
DEPRECATED_PLACEHOLDER_SYMBOL_STRING: 'Symbol(react.placeholder)',
176172
SCOPE_NUMBER: 0xead7,
177173
SCOPE_SYMBOL_STRING: 'Symbol(react.scope)',
178174
};
@@ -227,6 +223,7 @@ export function getInternalReactConstants(
227223
Profiler: 12,
228224
SimpleMemoComponent: 15,
229225
SuspenseComponent: 13,
226+
SuspenseListComponent: 19, // Experimental
230227
YieldComponent: -1, // Removed
231228
};
232229
} else if (gte(version, '16.4.3-alpha')) {
@@ -252,6 +249,7 @@ export function getInternalReactConstants(
252249
Profiler: 15,
253250
SimpleMemoComponent: -1, // Doesn't exist yet
254251
SuspenseComponent: 16,
252+
SuspenseListComponent: -1, // Doesn't exist yet
255253
YieldComponent: -1, // Removed
256254
};
257255
} else {
@@ -277,6 +275,7 @@ export function getInternalReactConstants(
277275
Profiler: 15,
278276
SimpleMemoComponent: -1, // Doesn't exist yet
279277
SuspenseComponent: 16,
278+
SuspenseListComponent: -1, // Doesn't exist yet
280279
YieldComponent: 9,
281280
};
282281
}
@@ -307,6 +306,8 @@ export function getInternalReactConstants(
307306
Fragment,
308307
MemoComponent,
309308
SimpleMemoComponent,
309+
SuspenseComponent,
310+
SuspenseListComponent,
310311
} = ReactTypeOfWork;
311312

312313
const {
@@ -319,9 +320,6 @@ export function getInternalReactConstants(
319320
CONTEXT_CONSUMER_SYMBOL_STRING,
320321
STRICT_MODE_NUMBER,
321322
STRICT_MODE_SYMBOL_STRING,
322-
SUSPENSE_NUMBER,
323-
SUSPENSE_SYMBOL_STRING,
324-
DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
325323
PROFILER_NUMBER,
326324
PROFILER_SYMBOL_STRING,
327325
SCOPE_NUMBER,
@@ -370,6 +368,10 @@ export function getInternalReactConstants(
370368
} else {
371369
return getDisplayName(type, 'Anonymous');
372370
}
371+
case SuspenseComponent:
372+
return 'Suspense';
373+
case SuspenseListComponent:
374+
return 'SuspenseList';
373375
default:
374376
const typeSymbol = getTypeSymbol(type);
375377

@@ -398,10 +400,6 @@ export function getInternalReactConstants(
398400
case STRICT_MODE_NUMBER:
399401
case STRICT_MODE_SYMBOL_STRING:
400402
return null;
401-
case SUSPENSE_NUMBER:
402-
case SUSPENSE_SYMBOL_STRING:
403-
case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
404-
return 'Suspense';
405403
case PROFILER_NUMBER:
406404
case PROFILER_SYMBOL_STRING:
407405
return `Profiler(${fiber.memoizedProps.id})`;
@@ -457,6 +455,7 @@ export function attach(
457455
MemoComponent,
458456
SimpleMemoComponent,
459457
SuspenseComponent,
458+
SuspenseListComponent,
460459
} = ReactTypeOfWork;
461460
const {
462461
ImmediatePriority,
@@ -478,9 +477,6 @@ export function attach(
478477
PROFILER_SYMBOL_STRING,
479478
STRICT_MODE_NUMBER,
480479
STRICT_MODE_SYMBOL_STRING,
481-
SUSPENSE_NUMBER,
482-
SUSPENSE_SYMBOL_STRING,
483-
DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
484480
} = ReactSymbols;
485481

486482
const {
@@ -711,6 +707,10 @@ export function attach(
711707
case MemoComponent:
712708
case SimpleMemoComponent:
713709
return ElementTypeMemo;
710+
case SuspenseComponent:
711+
return ElementTypeSuspense;
712+
case SuspenseListComponent:
713+
return ElementTypeSuspenseList;
714714
default:
715715
const typeSymbol = getTypeSymbol(type);
716716

@@ -728,10 +728,6 @@ export function attach(
728728
case STRICT_MODE_NUMBER:
729729
case STRICT_MODE_SYMBOL_STRING:
730730
return ElementTypeOtherOrUnknown;
731-
case SUSPENSE_NUMBER:
732-
case SUSPENSE_SYMBOL_STRING:
733-
case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
734-
return ElementTypeSuspense;
735731
case PROFILER_NUMBER:
736732
case PROFILER_SYMBOL_STRING:
737733
return ElementTypeProfiler;

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ export const ElementTypeOtherOrUnknown = 9;
3131
export const ElementTypeProfiler = 10;
3232
export const ElementTypeRoot = 11;
3333
export const ElementTypeSuspense = 12;
34+
export const ElementTypeSuspenseList = 13;
3435

3536
// Different types of elements displayed in the Elements tree.
3637
// These types may be used to visually distinguish types,
3738
// or to enable/disable certain functionality.
38-
export type ElementType = 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
39+
export type ElementType = 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13;
3940

4041
// WARNING
4142
// The values below are referenced by ComponentFilters (which are saved via localStorage).

packages/react-devtools-shell/src/app/SuspenseTree/index.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @flow
88
*/
99

10-
import React, {Fragment, Suspense, useState} from 'react';
10+
import React, {Fragment, Suspense, SuspenseList, useState} from 'react';
1111

1212
function SuspenseTree() {
1313
return (
@@ -18,6 +18,7 @@ function SuspenseTree() {
1818
<h4>Fallback to Primary Cycle</h4>
1919
<PrimaryFallbackTest initialSuspend={true} />
2020
<NestedSuspenseTest />
21+
<SuspenseListTest />
2122
</Fragment>
2223
);
2324
}
@@ -102,6 +103,29 @@ function Parent() {
102103
);
103104
}
104105

106+
function SuspenseListTest() {
107+
return (
108+
<>
109+
<h1>SuspenseList</h1>
110+
<SuspenseList revealOrder="forwards" tail="collapsed">
111+
<div>
112+
<Suspense fallback={<Fallback1>Loading 1</Fallback1>}>
113+
<Primary1>Hello</Primary1>
114+
</Suspense>
115+
</div>
116+
<div>
117+
<LoadLater />
118+
</div>
119+
<div>
120+
<Suspense fallback={<Fallback2>Loading 2</Fallback2>}>
121+
<Primary2>World</Primary2>
122+
</Suspense>
123+
</div>
124+
</SuspenseList>
125+
</>
126+
);
127+
}
128+
105129
function LoadLater() {
106130
const [loadChild, setLoadChild] = useState(0);
107131
return (

0 commit comments

Comments
 (0)