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
Sebastian Markbåge committed
Oct 18, 2019 at 17:18 UTC
3cc564547c81b6e9a3bd6a04aa0725e668ca6559
5 files changed
+95
-22
packages/react-devtools-shared/src/__tests__/__snapshots__/store-test.js.snap
+19
@@ -16,6 +16,25 @@ exports[`Store collapseNodesByDefault:false should display Suspense nodes proper
16
<Component key="Inside">
17
`;
18
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
+
38
exports[`Store collapseNodesByDefault:false should filter DOM nodes from the store tree: 1: mount 1`] = `
39
[root]
40
▾ <Grandparent>
packages/react-devtools-shared/src/__tests__/store-test.js
+33
@@ -342,6 +342,39 @@ describe('Store', () => {
342
expect(store).toMatchSnapshot('13: third child is suspended');
343
});
344
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
+
378
it('should support collapsing parts of the tree', () => {
379
const Grandparent = ({count}) => (
380
<React.Fragment>
packages/react-devtools-shared/src/backend/renderer.js
+16
-20
@@ -23,6 +23,7 @@ import {
23
ElementTypeProfiler,
24
ElementTypeRoot,
25
ElementTypeSuspense,
26
+ ElementTypeSuspenseList,
27
} from 'react-devtools-shared/src/types';
28
import {
29
getDisplayName,
@@ -91,9 +92,6 @@ type ReactSymbolsType = {
92
PROFILER_SYMBOL_STRING: string,
93
STRICT_MODE_NUMBER: number,
94
STRICT_MODE_SYMBOL_STRING: string,
94
- SUSPENSE_NUMBER: number,
95
- SUSPENSE_SYMBOL_STRING: string,
96
- DEPRECATED_PLACEHOLDER_SYMBOL_STRING: string,
95
SCOPE_NUMBER: number,
96
SCOPE_SYMBOL_STRING: string,
97
};
@@ -129,6 +127,7 @@ type ReactTypeOfWorkType = {|
127
Profiler: number,
128
SimpleMemoComponent: number,
129
SuspenseComponent: number,
130
+ SuspenseListComponent: number,
131
YieldComponent: number,
132
|};
133
@@ -170,9 +169,6 @@ export function getInternalReactConstants(
169
PROFILER_SYMBOL_STRING: 'Symbol(react.profiler)',
170
STRICT_MODE_NUMBER: 0xeacc,
171
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)',
172
SCOPE_NUMBER: 0xead7,
173
SCOPE_SYMBOL_STRING: 'Symbol(react.scope)',
174
};
@@ -227,6 +223,7 @@ export function getInternalReactConstants(
223
Profiler: 12,
224
SimpleMemoComponent: 15,
225
SuspenseComponent: 13,
226
+ SuspenseListComponent: 19, // Experimental
227
YieldComponent: -1, // Removed
228
};
229
} else if (gte(version, '16.4.3-alpha')) {
@@ -252,6 +249,7 @@ export function getInternalReactConstants(
249
Profiler: 15,
250
SimpleMemoComponent: -1, // Doesn't exist yet
251
SuspenseComponent: 16,
252
+ SuspenseListComponent: -1, // Doesn't exist yet
253
YieldComponent: -1, // Removed
254
};
255
} else {
@@ -277,6 +275,7 @@ export function getInternalReactConstants(
275
Profiler: 15,
276
SimpleMemoComponent: -1, // Doesn't exist yet
277
SuspenseComponent: 16,
278
+ SuspenseListComponent: -1, // Doesn't exist yet
279
YieldComponent: 9,
280
};
281
}
@@ -307,6 +306,8 @@ export function getInternalReactConstants(
306
Fragment,
307
MemoComponent,
308
SimpleMemoComponent,
309
+ SuspenseComponent,
310
+ SuspenseListComponent,
311
} = ReactTypeOfWork;
312
313
const {
@@ -319,9 +320,6 @@ export function getInternalReactConstants(
320
CONTEXT_CONSUMER_SYMBOL_STRING,
321
STRICT_MODE_NUMBER,
322
STRICT_MODE_SYMBOL_STRING,
322
- SUSPENSE_NUMBER,
323
- SUSPENSE_SYMBOL_STRING,
324
- DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
323
PROFILER_NUMBER,
324
PROFILER_SYMBOL_STRING,
325
SCOPE_NUMBER,
@@ -370,6 +368,10 @@ export function getInternalReactConstants(
368
} else {
369
return getDisplayName(type, 'Anonymous');
370
}
371
+ case SuspenseComponent:
372
+ return 'Suspense';
373
+ case SuspenseListComponent:
374
+ return 'SuspenseList';
375
default:
376
const typeSymbol = getTypeSymbol(type);
377
@@ -398,10 +400,6 @@ export function getInternalReactConstants(
400
case STRICT_MODE_NUMBER:
401
case STRICT_MODE_SYMBOL_STRING:
402
return null;
401
- case SUSPENSE_NUMBER:
402
- case SUSPENSE_SYMBOL_STRING:
403
- case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
404
- return 'Suspense';
403
case PROFILER_NUMBER:
404
case PROFILER_SYMBOL_STRING:
405
return `Profiler(${fiber.memoizedProps.id})`;
@@ -457,6 +455,7 @@ export function attach(
455
MemoComponent,
456
SimpleMemoComponent,
457
SuspenseComponent,
458
+ SuspenseListComponent,
459
} = ReactTypeOfWork;
460
const {
461
ImmediatePriority,
@@ -478,9 +477,6 @@ export function attach(
477
PROFILER_SYMBOL_STRING,
478
STRICT_MODE_NUMBER,
479
STRICT_MODE_SYMBOL_STRING,
481
- SUSPENSE_NUMBER,
482
- SUSPENSE_SYMBOL_STRING,
483
- DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
480
} = ReactSymbols;
481
482
const {
@@ -711,6 +707,10 @@ export function attach(
707
case MemoComponent:
708
case SimpleMemoComponent:
709
return ElementTypeMemo;
710
+ case SuspenseComponent:
711
+ return ElementTypeSuspense;
712
+ case SuspenseListComponent:
713
+ return ElementTypeSuspenseList;
714
default:
715
const typeSymbol = getTypeSymbol(type);
716
@@ -728,10 +728,6 @@ export function attach(
728
case STRICT_MODE_NUMBER:
729
case STRICT_MODE_SYMBOL_STRING:
730
return ElementTypeOtherOrUnknown;
731
- case SUSPENSE_NUMBER:
732
- case SUSPENSE_SYMBOL_STRING:
733
- case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
734
- return ElementTypeSuspense;
731
case PROFILER_NUMBER:
732
case PROFILER_SYMBOL_STRING:
733
return ElementTypeProfiler;
packages/react-devtools-shared/src/types.js
+2
-1
@@ -31,11 +31,12 @@ export const ElementTypeOtherOrUnknown = 9;
31
export const ElementTypeProfiler = 10;
32
export const ElementTypeRoot = 11;
33
export const ElementTypeSuspense = 12;
34
+export const ElementTypeSuspenseList = 13;
35
36
// Different types of elements displayed in the Elements tree.
37
// These types may be used to visually distinguish types,
38
// 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;
40
41
// WARNING
42
// The values below are referenced by ComponentFilters (which are saved via localStorage).
packages/react-devtools-shell/src/app/SuspenseTree/index.js
+25
-1
@@ -7,7 +7,7 @@
7
* @flow
8
*/
9
10
-import React, {Fragment, Suspense, useState} from 'react';
10
+import React, {Fragment, Suspense, SuspenseList, useState} from 'react';
11
12
function SuspenseTree() {
13
return (
@@ -18,6 +18,7 @@ function SuspenseTree() {
18
<h4>Fallback to Primary Cycle</h4>
19
<PrimaryFallbackTest initialSuspend={true} />
20
<NestedSuspenseTest />
21
+ <SuspenseListTest />
22
</Fragment>
23
);
24
}
@@ -102,6 +103,29 @@ function Parent() {
103
);
104
}
105
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
+
129
function LoadLater() {
130
const [loadChild, setLoadChild] = useState(0);
131
return (