Skip to content

Commit beae8b5

Browse files
committed
f
1 parent 7972aba commit beae8b5

File tree

3 files changed

+57
-75
lines changed

3 files changed

+57
-75
lines changed

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

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ import {
2121
} from '../Components/TreeContext';
2222
import {useHighlightHostInstance} from '../hooks';
2323
import styles from './SuspenseRects.css';
24-
import {
25-
SuspenseTreeStateContext,
26-
useSuspenseStore,
27-
} from './SuspenseTreeContext';
24+
import {useSuspenseStore} from './SuspenseTreeContext';
2825
import typeof {
2926
SyntheticMouseEvent,
3027
SyntheticPointerEvent,
@@ -111,9 +108,9 @@ function SuspenseRects({
111108

112109
function getDocumentBoundingRect(
113110
store: Store,
114-
shells: $ReadOnlyArray<SuspenseNode['id']>,
111+
roots: $ReadOnlyArray<SuspenseNode['id']>,
115112
): Rect {
116-
if (shells.length === 0) {
113+
if (roots.length === 0) {
117114
return {x: 0, y: 0, width: 0, height: 0};
118115
}
119116

@@ -122,14 +119,14 @@ function getDocumentBoundingRect(
122119
let maxX = Number.NEGATIVE_INFINITY;
123120
let maxY = Number.NEGATIVE_INFINITY;
124121

125-
for (let i = 0; i < shells.length; i++) {
126-
const shellID = shells[i];
127-
const shell = store.getSuspenseByID(shellID);
128-
if (shell === null) {
122+
for (let i = 0; i < roots.length; i++) {
123+
const rootID = roots[i];
124+
const root = store.getSuspenseByID(rootID);
125+
if (root === null) {
129126
continue;
130127
}
131128

132-
const rects = shell.rects;
129+
const rects = root.rects;
133130
if (rects === null) {
134131
continue;
135132
}
@@ -156,20 +153,20 @@ function getDocumentBoundingRect(
156153
}
157154

158155
function SuspenseRectsShell({
159-
shellID,
156+
rootID,
160157
}: {
161-
shellID: SuspenseNode['id'],
158+
rootID: SuspenseNode['id'],
162159
}): React$Node {
163160
const store = useSuspenseStore();
164-
const shell = store.getSuspenseByID(shellID);
165-
if (shell === null) {
166-
console.warn(`<Element> Could not find suspense node id ${shellID}`);
161+
const root = store.getSuspenseByID(rootID);
162+
if (root === null) {
163+
console.warn(`<Element> Could not find suspense node id ${rootID}`);
167164
return null;
168165
}
169166

170167
return (
171168
<g>
172-
{shell.children.map(childID => {
169+
{root.children.map(childID => {
173170
return <SuspenseRects key={childID} suspenseID={childID} />;
174171
})}
175172
</g>
@@ -179,9 +176,9 @@ function SuspenseRectsShell({
179176
function SuspenseRectsContainer(): React$Node {
180177
const store = useSuspenseStore();
181178
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
182-
const {shells} = useContext(SuspenseTreeStateContext);
179+
const roots = store.roots;
183180

184-
const boundingRect = getDocumentBoundingRect(store, shells);
181+
const boundingRect = getDocumentBoundingRect(store, roots);
185182

186183
const width = '100%';
187184
const boundingRectWidth = boundingRect.width;
@@ -195,8 +192,8 @@ function SuspenseRectsContainer(): React$Node {
195192
<svg
196193
style={{width, height}}
197194
viewBox={`${boundingRect.x} ${boundingRect.y} ${boundingRect.width} ${boundingRect.height}`}>
198-
{shells.map(shellID => {
199-
return <SuspenseRectsShell key={shellID} shellID={shellID} />;
195+
{roots.map(rootID => {
196+
return <SuspenseRectsShell key={rootID} rootID={rootID} />;
200197
})}
201198
</svg>
202199
</div>

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@ import {useContext, useLayoutEffect, useMemo, useRef, useState} from 'react';
1515
import {BridgeContext} from '../context';
1616
import {TreeDispatcherContext} from '../Components/TreeContext';
1717
import {useHighlightHostInstance} from '../hooks';
18-
import {
19-
SuspenseTreeStateContext,
20-
useSuspenseStore,
21-
} from './SuspenseTreeContext';
18+
import {useSuspenseStore} from './SuspenseTreeContext';
2219
import styles from './SuspenseTimeline.css';
2320
import typeof {
2421
SyntheticEvent,
@@ -218,9 +215,9 @@ function SuspenseTimelineInput({rootID}: {rootID: Element['id'] | void}) {
218215

219216
export default function SuspenseTimeline(): React$Node {
220217
const store = useSuspenseStore();
221-
const {shells} = useContext(SuspenseTreeStateContext);
222218

223-
const defaultSelectedRootID = shells.find(rootID => {
219+
const roots = store.roots;
220+
const defaultSelectedRootID = roots.find(rootID => {
224221
const suspense = store.getSuspenseByID(rootID);
225222
return (
226223
store.supportsTogglingSuspense(rootID) &&
@@ -243,13 +240,13 @@ export default function SuspenseTimeline(): React$Node {
243240
return (
244241
<div className={styles.SuspenseTimelineContainer}>
245242
<SuspenseTimelineInput key={selectedRootID} rootID={selectedRootID} />
246-
{shells.length > 0 && (
243+
{roots.length > 0 && (
247244
<select
248245
aria-label="Select Suspense Root"
249246
className={styles.SuspenseTimelineRootSwitcher}
250247
onChange={handleChange}
251248
value={selectedRootID}>
252-
{shells.map(rootID => {
249+
{roots.map(rootID => {
253250
// TODO: Use name
254251
const name = '#' + rootID;
255252
// TODO: Highlight host on hover

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

Lines changed: 34 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
* @flow
88
*/
99
import type {ReactContext} from 'shared/ReactTypes';
10-
import type {SuspenseNode} from '../../../frontend/types';
1110
import type Store from '../../store';
1211

1312
import * as React from 'react';
@@ -21,14 +20,10 @@ import {
2120
} from 'react';
2221
import {StoreContext} from '../context';
2322

24-
export type SuspenseTreeState = {
25-
shells: $ReadOnlyArray<SuspenseNode['id']>,
26-
};
23+
export type SuspenseTreeState = {};
2724

28-
type ACTION_HANDLE_SUSPENSE_TREE_MUTATION = {
29-
type: 'HANDLE_SUSPENSE_TREE_MUTATION',
30-
};
31-
export type SuspenseTreeAction = ACTION_HANDLE_SUSPENSE_TREE_MUTATION;
25+
// unused for now
26+
export type SuspenseTreeAction = {type: 'unused'};
3227
export type SuspenseTreeDispatch = (action: SuspenseTreeAction) => void;
3328

3429
const SuspenseTreeStateContext: ReactContext<SuspenseTreeState> =
@@ -43,11 +38,39 @@ type Props = {
4338
children: React$Node,
4439
};
4540

46-
function SuspenseTreeContextController({children}: Props): React.Node {
41+
/**
42+
* The Store is mutable. This Hook ensures renders read the latest Suspense related
43+
* data.
44+
*/
45+
function useSuspenseStore(): Store {
4746
const store = useContext(StoreContext);
48-
47+
const [, storeUpdated] = useReducer<number, number, void>(
48+
(x: number) => (x + 1) % Number.MAX_SAFE_INTEGER,
49+
0,
50+
);
4951
const initialRevision = useMemo(() => store.revisionSuspense, [store]);
52+
// We're currently storing everything Suspense related in the same Store as
53+
// Components. However, most reads are currently stateless. This ensures
54+
// the latest state is always read from the Store.
55+
useEffect(() => {
56+
const handleSuspenseTreeMutated = () => {
57+
storeUpdated();
58+
};
59+
60+
// Since this is a passive effect, the tree may have been mutated before our initial subscription.
61+
if (store.revisionSuspense !== initialRevision) {
62+
// At the moment, we can treat this as a mutation.
63+
handleSuspenseTreeMutated();
64+
}
5065

66+
store.addListener('suspenseTreeMutated', handleSuspenseTreeMutated);
67+
return () =>
68+
store.removeListener('suspenseTreeMutated', handleSuspenseTreeMutated);
69+
}, [initialRevision, store]);
70+
return store;
71+
}
72+
73+
function SuspenseTreeContextController({children}: Props): React.Node {
5174
// This reducer is created inline because it needs access to the Store.
5275
// The store is mutable, but the Store itself is global and lives for the lifetime of the DevTools,
5376
// so it's okay for the reducer to have an empty dependencies array.
@@ -59,18 +82,14 @@ function SuspenseTreeContextController({children}: Props): React.Node {
5982
): SuspenseTreeState => {
6083
const {type} = action;
6184
switch (type) {
62-
case 'HANDLE_SUSPENSE_TREE_MUTATION':
63-
return {...state, shells: store.roots};
6485
default:
6586
throw new Error(`Unrecognized action "${type}"`);
6687
}
6788
},
6889
[],
6990
);
7091

71-
const initialState: SuspenseTreeState = {
72-
shells: store.roots,
73-
};
92+
const initialState: SuspenseTreeState = {};
7493
const [state, dispatch] = useReducer(reducer, initialState);
7594
const transitionDispatch = useMemo(
7695
() => (action: SuspenseTreeAction) =>
@@ -80,28 +99,6 @@ function SuspenseTreeContextController({children}: Props): React.Node {
8099
[dispatch],
81100
);
82101

83-
useEffect(() => {
84-
const handleSuspenseTreeMutated = () => {
85-
dispatch({
86-
type: 'HANDLE_SUSPENSE_TREE_MUTATION',
87-
});
88-
};
89-
90-
// Since this is a passive effect, the tree may have been mutated before our initial subscription.
91-
if (store.revisionSuspense !== initialRevision) {
92-
// At the moment, we can treat this as a mutation.
93-
// We don't know which Elements were newly added/removed, but that should be okay in this case.
94-
// It would only impact the search state, which is unlikely to exist yet at this point.
95-
dispatch({
96-
type: 'HANDLE_SUSPENSE_TREE_MUTATION',
97-
});
98-
}
99-
100-
store.addListener('suspenseTreeMutated', handleSuspenseTreeMutated);
101-
return () =>
102-
store.removeListener('suspenseTreeMutated', handleSuspenseTreeMutated);
103-
}, [dispatch, initialRevision, store]);
104-
105102
return (
106103
<SuspenseTreeStateContext.Provider value={state}>
107104
<SuspenseTreeDispatcherContext.Provider value={transitionDispatch}>
@@ -111,15 +108,6 @@ function SuspenseTreeContextController({children}: Props): React.Node {
111108
);
112109
}
113110

114-
function useSuspenseStore(): Store {
115-
const store = useContext(StoreContext);
116-
// We're currently storing everything Suspense related in the same Store as
117-
// Components. However, most reads are currently stateless. This ensures
118-
// the latest state is always read from the Store.
119-
useContext(SuspenseTreeStateContext);
120-
return store;
121-
}
122-
123111
export {
124112
SuspenseTreeDispatcherContext,
125113
SuspenseTreeStateContext,

0 commit comments

Comments
 (0)