Allow to toggle Suspense in Components pane
Dan Abramov committed
Apr 4, 2019 at 15:26 UTC
4f5437edf767f153152d14e2565c777921dd1a47
6 files changed
+119
-8
shells/dev/app/index.js
+1
@@ -32,6 +32,7 @@ function mountTestApp() {
32
mountHelper(ElementTypes);
33
mountHelper(EditableProps);
34
mountHelper(DeeplyNestedComponents);
35
+ mountHelper(SuspenseTree);
36
}
37
38
function unmountTestApp() {
src/backend/agent.js
+20
@@ -38,6 +38,12 @@ type SetInParams = {|
38
value: any,
39
|};
40
41
+type OverrideSuspenseParams = {|
42
+ id: number,
43
+ rendererID: number,
44
+ forceFallback: boolean,
45
+|};
46
+
47
export default class Agent extends EventEmitter {
48
_bridge: Bridge = ((null: any): Bridge);
49
_isProfiling: boolean = false;
@@ -68,6 +74,7 @@ export default class Agent extends EventEmitter {
74
bridge.addListener('overrideHookState', this.overrideHookState);
75
bridge.addListener('overrideProps', this.overrideProps);
76
bridge.addListener('overrideState', this.overrideState);
77
+ bridge.addListener('overrideSuspense', this.overrideSuspense);
78
bridge.addListener('reloadAndProfile', this.reloadAndProfile);
79
bridge.addListener('screenshotCaptured', this.screenshotCaptured);
80
bridge.addListener('selectElement', this.selectElement);
@@ -302,6 +309,19 @@ export default class Agent extends EventEmitter {
309
}
310
};
311
312
+ overrideSuspense = ({
313
+ id,
314
+ rendererID,
315
+ forceFallback,
316
+ }: OverrideSuspenseParams) => {
317
+ const renderer = this._rendererInterfaces[rendererID];
318
+ if (renderer == null) {
319
+ console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
320
+ } else {
321
+ renderer.overrideSuspense(id, forceFallback);
322
+ }
323
+ };
324
+
325
setRendererInterface(
326
rendererID: RendererID,
327
rendererInterface: RendererInterface
src/backend/renderer.js
+62
-3
@@ -199,6 +199,7 @@ export function attach(
199
IndeterminateComponent,
200
MemoComponent,
201
SimpleMemoComponent,
202
+ SuspenseComponent,
203
} = ReactTypeOfWork;
204
const {
205
CONCURRENT_MODE_NUMBER,
@@ -219,7 +220,15 @@ export function attach(
220
DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
221
} = ReactSymbols;
222
222
- const { overrideHookState, overrideProps } = renderer;
223
+ const {
224
+ overrideHookState,
225
+ overrideProps,
226
+ setSuspenseHandler,
227
+ scheduleUpdate,
228
+ } = renderer;
229
+ const supportsEditingSuspense =
230
+ typeof setSuspenseHandler === 'function' &&
231
+ typeof scheduleUpdate === 'function';
232
233
const debug = (name: string, fiber: Fiber, parentFiber: ?Fiber): void => {
234
if (__DEBUG__) {
@@ -845,8 +854,7 @@ export function attach(
854
855
// Suspense components only have a non-null memoizedState if they're timed-out.
856
const isTimedOutSuspense =
848
- nextFiber.tag === ReactTypeOfWork.SuspenseComponent &&
849
- nextFiber.memoizedState !== null;
857
+ nextFiber.tag === SuspenseComponent && nextFiber.memoizedState !== null;
858
859
if (isTimedOutSuspense) {
860
// The behavior of timed-out Suspense trees is unique.
@@ -1411,6 +1419,9 @@ export function attach(
1419
}
1420
}
1421
1422
+ const isTimedOutSuspense =
1423
+ tag === SuspenseComponent && memoizedState !== null;
1424
+
1425
return {
1426
id,
1427
@@ -1420,6 +1431,14 @@ export function attach(
1431
// Does the current renderer support editable function props?
1432
canEditFunctionProps: typeof overrideProps === 'function',
1433
1434
+ canEditSuspense:
1435
+ supportsEditingSuspense &&
1436
+ // If it's showing the real content, we can always flip fallback.
1437
+ (!isTimedOutSuspense ||
1438
+ // If it's showing fallback because we previously forced it to,
1439
+ // allow toggling it back to remove the fallback override.
1440
+ forceFallbackForSuspenseIDs.has(id)),
1441
+
1442
// Can view component source location.
1443
canViewSource,
1444
@@ -1667,6 +1686,45 @@ export function attach(
1686
startProfiling();
1687
}
1688
1689
+ // React will switch between these implementations depending on whether
1690
+ // we have any manually suspended Fibers or not.
1691
+
1692
+ function shouldSuspendFiberAlwaysFalse() {
1693
+ return false;
1694
+ }
1695
+
1696
+ let forceFallbackForSuspenseIDs = new Set();
1697
+ function shouldSuspendFiberAccordingToSet(fiber) {
1698
+ const id = getFiberID(getPrimaryFiber(((fiber: any): Fiber)));
1699
+ return forceFallbackForSuspenseIDs.has(id);
1700
+ }
1701
+
1702
+ function overrideSuspense(id, forceFallback) {
1703
+ if (
1704
+ typeof setSuspenseHandler !== 'function' ||
1705
+ typeof scheduleUpdate !== 'function'
1706
+ ) {
1707
+ throw new Error(
1708
+ 'Expected overrideSuspense() to not get called for earlier React versions.'
1709
+ );
1710
+ }
1711
+ if (forceFallback) {
1712
+ forceFallbackForSuspenseIDs.add(id);
1713
+ if (forceFallbackForSuspenseIDs.size === 1) {
1714
+ // First override is added. Switch React to slower path.
1715
+ setSuspenseHandler(shouldSuspendFiberAccordingToSet);
1716
+ }
1717
+ } else {
1718
+ forceFallbackForSuspenseIDs.delete(id);
1719
+ if (forceFallbackForSuspenseIDs.size === 0) {
1720
+ // Last override is gone. Switch React back to fast path.
1721
+ setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
1722
+ }
1723
+ }
1724
+ const fiber = idToFiberMap.get(id);
1725
+ scheduleUpdate(fiber);
1726
+ }
1727
+
1728
return {
1729
cleanup,
1730
flushInitialOperations,
@@ -1680,6 +1738,7 @@ export function attach(
1738
handleCommitFiberUnmount,
1739
inspectElement,
1740
prepareViewElementSource,
1741
+ overrideSuspense,
1742
renderer,
1743
selectElement,
1744
setInContext,
src/backend/types.js
+5
@@ -45,6 +45,10 @@ export type ReactRenderer = {
45
value: any
46
) => void,
47
48
+ // 16.9+
49
+ scheduleUpdate?: ?(fiber: Object) => void,
50
+ setSuspenseHandler?: ?(shouldSuspend: (fiber: Object) => boolean) => void,
51
+
52
// Only injected by React v16.8+ in order to support hooks inspection.
53
currentDispatcherRef?: {| current: null | Dispatcher |},
54
};
@@ -95,6 +99,7 @@ export type RendererInterface = {
99
handleCommitFiberRoot: (fiber: Object) => void,
100
handleCommitFiberUnmount: (fiber: Object) => void,
101
inspectElement: (id: number) => InspectedElement | null,
102
+ overrideSuspense: (id: number, forceFallback: boolean) => void,
103
prepareViewElementSource: (id: number) => void,
104
renderer: ReactRenderer | null,
105
selectElement: (id: number) => void,
src/devtools/views/Components/SelectedElement.js
+28
-5
@@ -20,6 +20,7 @@ import {
20
ElementTypeForwardRef,
21
ElementTypeFunction,
22
ElementTypeMemo,
23
+ ElementTypeSuspense,
24
} from '../../types';
25
26
import type { InspectedElement } from './types';
@@ -115,6 +116,8 @@ type InspectedElementViewProps = {|
116
inspectedElement: InspectedElement,
117
|};
118
119
+const IS_SUSPENDED = 'Suspended';
120
+
121
function InspectedElementView({
122
element,
123
inspectedElement,
@@ -123,6 +126,7 @@ function InspectedElementView({
126
const {
127
canEditFunctionProps,
128
canEditHooks,
129
+ canEditSuspense,
130
context,
131
hooks,
132
owners,
@@ -137,6 +141,7 @@ function InspectedElementView({
141
let overrideContextFn = null;
142
let overridePropsFn = null;
143
let overrideStateFn = null;
144
+ let overrideSuspenseFn = null;
145
if (type === ElementTypeClass) {
146
overrideContextFn = (path: Array<string | number>, value: any) => {
147
const rendererID = store.getRendererIDForElement(id);
@@ -160,6 +165,14 @@ function InspectedElementView({
165
const rendererID = store.getRendererIDForElement(id);
166
bridge.send('overrideProps', { id, path, rendererID, value });
167
};
168
+ } else if (type === ElementTypeSuspense && canEditSuspense) {
169
+ overrideSuspenseFn = (path: Array<string | number>, value: boolean) => {
170
+ if (path.length !== 1 && path !== IS_SUSPENDED) {
171
+ throw new Error('Unexpected path.');
172
+ }
173
+ const rendererID = store.getRendererIDForElement(id);
174
+ bridge.send('overrideSuspense', { id, rendererID, forceFallback: value });
175
+ };
176
}
177
178
return (
@@ -170,11 +183,21 @@ function InspectedElementView({
183
overrideValueFn={overridePropsFn}
184
showWhenEmpty
185
/>
173
- <InspectedElementTree
174
- label="state"
175
- data={state}
176
- overrideValueFn={overrideStateFn}
177
- />
186
+ {type === ElementTypeSuspense ? (
187
+ <InspectedElementTree
188
+ label="suspense"
189
+ data={{
190
+ [IS_SUSPENDED]: state !== null,
191
+ }}
192
+ overrideValueFn={overrideSuspenseFn}
193
+ />
194
+ ) : (
195
+ <InspectedElementTree
196
+ label="state"
197
+ data={state}
198
+ overrideValueFn={overrideStateFn}
199
+ />
200
+ )}
201
<HooksTree canEditHooks={canEditHooks} hooks={hooks} id={id} />
202
<InspectedElementTree
203
label="context"
src/devtools/views/Components/types.js
+3
@@ -41,6 +41,9 @@ export type InspectedElement = {|
41
// Does the current renderer support editable function props?
42
canEditFunctionProps: boolean,
43
44
+ // Is this Suspense, and can its value be overriden now?
45
+ canEditSuspense: boolean,
46
+
47
// Can view component source location.
48
canViewSource: boolean,
49