Merge pull request #61 from gaearon/toggle-suspense-2

Allow to toggle Suspense in Components pane
This commit is contained in:
Dan Abramov
2019-04-05 13:22:41 +01:00
committed by GitHub
5 changed files with 140 additions and 21 deletions
+22 -5
View File
@@ -38,6 +38,12 @@ type SetInParams = {|
value: any,
|};
type OverrideSuspenseParams = {|
id: number,
rendererID: number,
forceFallback: boolean,
|};
export default class Agent extends EventEmitter {
_bridge: Bridge = ((null: any): Bridge);
_isProfiling: boolean = false;
@@ -68,6 +74,7 @@ export default class Agent extends EventEmitter {
bridge.addListener('overrideHookState', this.overrideHookState);
bridge.addListener('overrideProps', this.overrideProps);
bridge.addListener('overrideState', this.overrideState);
bridge.addListener('overrideSuspense', this.overrideSuspense);
bridge.addListener('reloadAndProfile', this.reloadAndProfile);
bridge.addListener('screenshotCaptured', this.screenshotCaptured);
bridge.addListener('selectElement', this.selectElement);
@@ -203,11 +210,8 @@ export default class Agent extends EventEmitter {
}
let node: HTMLElement | null = null;
if (
renderer !== null &&
typeof renderer.getNativeFromReactElement === 'function'
) {
node = ((renderer.getNativeFromReactElement(id): any): HTMLElement);
if (renderer !== null) {
node = ((renderer.findNativeByFiberID(id): any): HTMLElement);
}
if (node != null) {
@@ -302,6 +306,19 @@ export default class Agent extends EventEmitter {
}
};
overrideSuspense = ({
id,
rendererID,
forceFallback,
}: OverrideSuspenseParams) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
} else {
renderer.overrideSuspense(id, forceFallback);
}
};
setRendererInterface(
rendererID: RendererID,
rendererInterface: RendererInterface
+81 -10
View File
@@ -199,6 +199,7 @@ export function attach(
IndeterminateComponent,
MemoComponent,
SimpleMemoComponent,
SuspenseComponent,
} = ReactTypeOfWork;
const {
CONCURRENT_MODE_NUMBER,
@@ -219,7 +220,15 @@ export function attach(
DEPRECATED_PLACEHOLDER_SYMBOL_STRING,
} = ReactSymbols;
const { overrideHookState, overrideProps } = renderer;
const {
overrideHookState,
overrideProps,
setSuspenseHandler,
scheduleUpdate,
} = renderer;
const supportsTogglingSuspense =
typeof setSuspenseHandler === 'function' &&
typeof scheduleUpdate === 'function';
const debug = (name: string, fiber: Fiber, parentFiber: ?Fiber): void => {
if (__DEBUG__) {
@@ -845,8 +854,7 @@ export function attach(
// Suspense components only have a non-null memoizedState if they're timed-out.
const isTimedOutSuspense =
nextFiber.tag === ReactTypeOfWork.SuspenseComponent &&
nextFiber.memoizedState !== null;
nextFiber.tag === SuspenseComponent && nextFiber.memoizedState !== null;
if (isTimedOutSuspense) {
// The behavior of timed-out Suspense trees is unique.
@@ -1045,18 +1053,30 @@ export function attach(
currentRootID = -1;
}
// The naming is confusing.
// They deal with opaque nodes (fibers), not elements.
function getNativeFromReactElement(id: number) {
function findNativeByFiberID(id: number) {
try {
const primaryFiber = getPrimaryFiber(idToFiberMap.get(id));
const hostInstance = renderer.findHostInstanceByFiber(primaryFiber);
return hostInstance;
const fiber = findCurrentFiberUsingSlowPath(idToFiberMap.get(id));
if (fiber === null) {
return null;
}
const isTimedOutSuspense =
fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
if (!isTimedOutSuspense) {
// Normal case.
return renderer.findHostInstanceByFiber(fiber);
} else {
// A timed-out Suspense's findDOMNode is useless.
// Try our best to find the fallback directly.
const maybeFallbackFiber =
(fiber.child && fiber.child.sibling) || fiber;
return renderer.findHostInstanceByFiber(maybeFallbackFiber);
}
} catch (err) {
// The fiber might have unmounted by now.
return null;
}
}
function getFiberIDFromNative(
hostInstance,
findNearestUnfilteredAncestor = false
@@ -1411,6 +1431,9 @@ export function attach(
}
}
const isTimedOutSuspense =
tag === SuspenseComponent && memoizedState !== null;
return {
id,
@@ -1420,6 +1443,14 @@ export function attach(
// Does the current renderer support editable function props?
canEditFunctionProps: typeof overrideProps === 'function',
canToggleSuspense:
supportsTogglingSuspense &&
// If it's showing the real content, we can always flip fallback.
(!isTimedOutSuspense ||
// If it's showing fallback because we previously forced it to,
// allow toggling it back to remove the fallback override.
forceFallbackForSuspenseIDs.has(id)),
// Can view component source location.
canViewSource,
@@ -1667,19 +1698,59 @@ export function attach(
startProfiling();
}
// React will switch between these implementations depending on whether
// we have any manually suspended Fibers or not.
function shouldSuspendFiberAlwaysFalse() {
return false;
}
let forceFallbackForSuspenseIDs = new Set();
function shouldSuspendFiberAccordingToSet(fiber) {
const id = getFiberID(getPrimaryFiber(((fiber: any): Fiber)));
return forceFallbackForSuspenseIDs.has(id);
}
function overrideSuspense(id, forceFallback) {
if (
typeof setSuspenseHandler !== 'function' ||
typeof scheduleUpdate !== 'function'
) {
throw new Error(
'Expected overrideSuspense() to not get called for earlier React versions.'
);
}
if (forceFallback) {
forceFallbackForSuspenseIDs.add(id);
if (forceFallbackForSuspenseIDs.size === 1) {
// First override is added. Switch React to slower path.
setSuspenseHandler(shouldSuspendFiberAccordingToSet);
}
} else {
forceFallbackForSuspenseIDs.delete(id);
if (forceFallbackForSuspenseIDs.size === 0) {
// Last override is gone. Switch React back to fast path.
setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
}
}
const fiber = idToFiberMap.get(id);
scheduleUpdate(fiber);
}
return {
cleanup,
flushInitialOperations,
getCommitDetails,
getFiberIDFromNative,
getInteractions,
getNativeFromReactElement,
findNativeByFiberID,
getProfilingDataForDownload,
getProfilingSummary,
handleCommitFiberRoot,
handleCommitFiberUnmount,
inspectElement,
prepareViewElementSource,
overrideSuspense,
renderer,
selectElement,
setInContext,
+6 -1
View File
@@ -45,6 +45,10 @@ export type ReactRenderer = {
value: any
) => void,
// 16.9+
scheduleUpdate?: ?(fiber: Object) => void,
setSuspenseHandler?: ?(shouldSuspend: (fiber: Object) => boolean) => void,
// Only injected by React v16.8+ in order to support hooks inspection.
currentDispatcherRef?: {| current: null | Dispatcher |},
};
@@ -82,9 +86,9 @@ export type ProfilingSummary = {|
export type RendererInterface = {
cleanup: () => void,
findNativeByFiberID: (id: number) => ?NativeType,
flushInitialOperations: () => void,
getCommitDetails: (rootID: number, commitIndex: number) => CommitDetails,
getNativeFromReactElement?: ?(component: Fiber) => ?NativeType,
getFiberIDFromNative: (
component: NativeType,
findNearestUnfilteredAncestor?: boolean
@@ -95,6 +99,7 @@ export type RendererInterface = {
handleCommitFiberRoot: (fiber: Object) => void,
handleCommitFiberUnmount: (fiber: Object) => void,
inspectElement: (id: number) => InspectedElement | null,
overrideSuspense: (id: number, forceFallback: boolean) => void,
prepareViewElementSource: (id: number) => void,
renderer: ReactRenderer | null,
selectElement: (id: number) => void,
@@ -20,6 +20,7 @@ import {
ElementTypeForwardRef,
ElementTypeFunction,
ElementTypeMemo,
ElementTypeSuspense,
} from '../../types';
import type { InspectedElement } from './types';
@@ -115,6 +116,8 @@ type InspectedElementViewProps = {|
inspectedElement: InspectedElement,
|};
const IS_SUSPENDED = 'Suspended';
function InspectedElementView({
element,
inspectedElement,
@@ -123,6 +126,7 @@ function InspectedElementView({
const {
canEditFunctionProps,
canEditHooks,
canToggleSuspense,
context,
hooks,
owners,
@@ -137,6 +141,7 @@ function InspectedElementView({
let overrideContextFn = null;
let overridePropsFn = null;
let overrideStateFn = null;
let overrideSuspenseFn = null;
if (type === ElementTypeClass) {
overrideContextFn = (path: Array<string | number>, value: any) => {
const rendererID = store.getRendererIDForElement(id);
@@ -160,6 +165,14 @@ function InspectedElementView({
const rendererID = store.getRendererIDForElement(id);
bridge.send('overrideProps', { id, path, rendererID, value });
};
} else if (type === ElementTypeSuspense && canToggleSuspense) {
overrideSuspenseFn = (path: Array<string | number>, value: boolean) => {
if (path.length !== 1 && path !== IS_SUSPENDED) {
throw new Error('Unexpected path.');
}
const rendererID = store.getRendererIDForElement(id);
bridge.send('overrideSuspense', { id, rendererID, forceFallback: value });
};
}
return (
@@ -170,11 +183,21 @@ function InspectedElementView({
overrideValueFn={overridePropsFn}
showWhenEmpty
/>
<InspectedElementTree
label="state"
data={state}
overrideValueFn={overrideStateFn}
/>
{type === ElementTypeSuspense ? (
<InspectedElementTree
label="suspense"
data={{
[IS_SUSPENDED]: state !== null,
}}
overrideValueFn={overrideSuspenseFn}
/>
) : (
<InspectedElementTree
label="state"
data={state}
overrideValueFn={overrideStateFn}
/>
)}
<HooksTree canEditHooks={canEditHooks} hooks={hooks} id={id} />
<InspectedElementTree
label="context"
+3
View File
@@ -41,6 +41,9 @@ export type InspectedElement = {|
// Does the current renderer support editable function props?
canEditFunctionProps: boolean,
// Is this Suspense, and can its value be overriden now?
canToggleSuspense: boolean,
// Can view component source location.
canViewSource: boolean,