@samitouri / QOS-React-2 / commits / 3cc564547c

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 (