Skip to content

Commit

Permalink
SuspenseList support in DevTools (#17145)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sebmarkbage committed Oct 19, 2019
1 parent 68fb580 commit 3cc5645
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ exports[`Store collapseNodesByDefault:false should display Suspense nodes proper
<Component key="Inside">
`;

exports[`Store collapseNodesByDefault:false should display a partially rendered SuspenseList: 1: loading 1`] = `
[root]
▾ <Wrapper>
▾ <SuspenseList>
<Component key="A">
▾ <Suspense>
<Loading>
`;

exports[`Store collapseNodesByDefault:false should display a partially rendered SuspenseList: 2: resolved 1`] = `
[root]
▾ <Wrapper>
▾ <SuspenseList>
<Component key="A">
▾ <Suspense>
<Component key="B">
<Component key="C">
`;

exports[`Store collapseNodesByDefault:false should filter DOM nodes from the store tree: 1: mount 1`] = `
[root]
▾ <Grandparent>
Expand Down
33 changes: 33 additions & 0 deletions packages/react-devtools-shared/src/__tests__/store-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,39 @@ describe('Store', () => {
expect(store).toMatchSnapshot('13: third child is suspended');
});

it('should display a partially rendered SuspenseList', () => {
const Loading = () => <div>Loading...</div>;
const SuspendingComponent = () => {
throw new Promise(() => {});
};
const Component = () => {
return <div>Hello</div>;
};
const Wrapper = ({shouldSuspense}) => (
<React.Fragment>
<React.SuspenseList revealOrder="forwards" tail="collapsed">
<Component key="A" />
<React.Suspense fallback={<Loading />}>
{shouldSuspense ? <SuspendingComponent /> : <Component key="B" />}
</React.Suspense>
<Component key="C" />
</React.SuspenseList>
</React.Fragment>
);

const container = document.createElement('div');
const root = ReactDOM.createRoot(container);
act(() => {
root.render(<Wrapper shouldSuspense={true} />);
});
expect(store).toMatchSnapshot('1: loading');

act(() => {
root.render(<Wrapper shouldSuspense={false} />);
});
expect(store).toMatchSnapshot('2: resolved');
});

it('should support collapsing parts of the tree', () => {
const Grandparent = ({count}) => (
<React.Fragment>
Expand Down
36 changes: 16 additions & 20 deletions packages/react-devtools-shared/src/backend/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
ElementTypeProfiler,
ElementTypeRoot,
ElementTypeSuspense,
ElementTypeSuspenseList,
} from 'react-devtools-shared/src/types';
import {
getDisplayName,
Expand Down Expand Up @@ -91,9 +92,6 @@ type ReactSymbolsType = {
PROFILER_SYMBOL_STRING: string,
STRICT_MODE_NUMBER: number,
STRICT_MODE_SYMBOL_STRING: string,
SUSPENSE_NUMBER: number,
SUSPENSE_SYMBOL_STRING: string,
DEPRECATED_PLACEHOLDER_SYMBOL_STRING: string,
SCOPE_NUMBER: number,
SCOPE_SYMBOL_STRING: string,
};
Expand Down Expand Up @@ -129,6 +127,7 @@ type ReactTypeOfWorkType = {|
Profiler: number,
SimpleMemoComponent: number,
SuspenseComponent: number,
SuspenseListComponent: number,
YieldComponent: number,
|};

Expand Down Expand Up @@ -170,9 +169,6 @@ export function getInternalReactConstants(
PROFILER_SYMBOL_STRING: 'Symbol(react.profiler)',
STRICT_MODE_NUMBER: 0xeacc,
STRICT_MODE_SYMBOL_STRING: 'Symbol(react.strict_mode)',
SUSPENSE_NUMBER: 0xead1,
SUSPENSE_SYMBOL_STRING: 'Symbol(react.suspense)',
DEPRECATED_PLACEHOLDER_SYMBOL_STRING: 'Symbol(react.placeholder)',
SCOPE_NUMBER: 0xead7,
SCOPE_SYMBOL_STRING: 'Symbol(react.scope)',
};
Expand Down Expand Up @@ -227,6 +223,7 @@ export function getInternalReactConstants(
Profiler: 12,
SimpleMemoComponent: 15,
SuspenseComponent: 13,
SuspenseListComponent: 19, // Experimental
YieldComponent: -1, // Removed
};
} else if (gte(version, '16.4.3-alpha')) {
Expand All @@ -252,6 +249,7 @@ export function getInternalReactConstants(
Profiler: 15,
SimpleMemoComponent: -1, // Doesn't exist yet
SuspenseComponent: 16,
SuspenseListComponent: -1, // Doesn't exist yet
YieldComponent: -1, // Removed
};
} else {
Expand All @@ -277,6 +275,7 @@ export function getInternalReactConstants(
Profiler: 15,
SimpleMemoComponent: -1, // Doesn't exist yet
SuspenseComponent: 16,
SuspenseListComponent: -1, // Doesn't exist yet
YieldComponent: 9,
};
}
Expand Down Expand Up @@ -307,6 +306,8 @@ export function getInternalReactConstants(
Fragment,
MemoComponent,
SimpleMemoComponent,
SuspenseComponent,
SuspenseListComponent,
} = ReactTypeOfWork;

const {
Expand All @@ -319,9 +320,6 @@ export function getInternalReactConstants(
CONTEXT_CONSUMER_SYMBOL_STRING,
STRICT_MODE_NUMBER,
STRICT_MODE_SYMBOL_STRING,
SUSPENSE_NUMBER,
SUSPENSE_SYMBOL_STRING,
DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
PROFILER_NUMBER,
PROFILER_SYMBOL_STRING,
SCOPE_NUMBER,
Expand Down Expand Up @@ -370,6 +368,10 @@ export function getInternalReactConstants(
} else {
return getDisplayName(type, 'Anonymous');
}
case SuspenseComponent:
return 'Suspense';
case SuspenseListComponent:
return 'SuspenseList';
default:
const typeSymbol = getTypeSymbol(type);

Expand Down Expand Up @@ -398,10 +400,6 @@ export function getInternalReactConstants(
case STRICT_MODE_NUMBER:
case STRICT_MODE_SYMBOL_STRING:
return null;
case SUSPENSE_NUMBER:
case SUSPENSE_SYMBOL_STRING:
case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
return 'Suspense';
case PROFILER_NUMBER:
case PROFILER_SYMBOL_STRING:
return `Profiler(${fiber.memoizedProps.id})`;
Expand Down Expand Up @@ -457,6 +455,7 @@ export function attach(
MemoComponent,
SimpleMemoComponent,
SuspenseComponent,
SuspenseListComponent,
} = ReactTypeOfWork;
const {
ImmediatePriority,
Expand All @@ -478,9 +477,6 @@ export function attach(
PROFILER_SYMBOL_STRING,
STRICT_MODE_NUMBER,
STRICT_MODE_SYMBOL_STRING,
SUSPENSE_NUMBER,
SUSPENSE_SYMBOL_STRING,
DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
} = ReactSymbols;

const {
Expand Down Expand Up @@ -711,6 +707,10 @@ export function attach(
case MemoComponent:
case SimpleMemoComponent:
return ElementTypeMemo;
case SuspenseComponent:
return ElementTypeSuspense;
case SuspenseListComponent:
return ElementTypeSuspenseList;
default:
const typeSymbol = getTypeSymbol(type);

Expand All @@ -728,10 +728,6 @@ export function attach(
case STRICT_MODE_NUMBER:
case STRICT_MODE_SYMBOL_STRING:
return ElementTypeOtherOrUnknown;
case SUSPENSE_NUMBER:
case SUSPENSE_SYMBOL_STRING:
case DEPRECATED_PLACEHOLDER_SYMBOL_STRING:
return ElementTypeSuspense;
case PROFILER_NUMBER:
case PROFILER_SYMBOL_STRING:
return ElementTypeProfiler;
Expand Down
3 changes: 2 additions & 1 deletion packages/react-devtools-shared/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ export const ElementTypeOtherOrUnknown = 9;
export const ElementTypeProfiler = 10;
export const ElementTypeRoot = 11;
export const ElementTypeSuspense = 12;
export const ElementTypeSuspenseList = 13;

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

// WARNING
// The values below are referenced by ComponentFilters (which are saved via localStorage).
Expand Down
26 changes: 25 additions & 1 deletion packages/react-devtools-shell/src/app/SuspenseTree/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @flow
*/

import React, {Fragment, Suspense, useState} from 'react';
import React, {Fragment, Suspense, SuspenseList, useState} from 'react';

function SuspenseTree() {
return (
Expand All @@ -18,6 +18,7 @@ function SuspenseTree() {
<h4>Fallback to Primary Cycle</h4>
<PrimaryFallbackTest initialSuspend={true} />
<NestedSuspenseTest />
<SuspenseListTest />
</Fragment>
);
}
Expand Down Expand Up @@ -102,6 +103,29 @@ function Parent() {
);
}

function SuspenseListTest() {
return (
<>
<h1>SuspenseList</h1>
<SuspenseList revealOrder="forwards" tail="collapsed">
<div>
<Suspense fallback={<Fallback1>Loading 1</Fallback1>}>
<Primary1>Hello</Primary1>
</Suspense>
</div>
<div>
<LoadLater />
</div>
<div>
<Suspense fallback={<Fallback2>Loading 2</Fallback2>}>
<Primary2>World</Primary2>
</Suspense>
</div>
</SuspenseList>
</>
);
}

function LoadLater() {
const [loadChild, setLoadChild] = useState(0);
return (
Expand Down

0 comments on commit 3cc5645

Please sign in to comment.