@samitouri / QOS-React / commits / 4f5437edf7

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