mirror of
https://github.com/zebrajr/react.git
synced 2026-01-15 12:15:22 +00:00
[DevTools] Batch Suspense toggles when advancing the Suspense timeline (#34251)
This commit is contained in:
committed by
GitHub
parent
cacc20e37c
commit
cb1e73be04
@@ -901,6 +901,94 @@ describe('Store', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
// @reactVersion >= 18.0
|
||||
it('can override multiple Suspense simultaneously', async () => {
|
||||
const Component = () => {
|
||||
return <div>Hello</div>;
|
||||
};
|
||||
const App = () => (
|
||||
<React.Fragment>
|
||||
<Component key="Outside" />
|
||||
<React.Suspense
|
||||
name="parent"
|
||||
fallback={<Component key="Parent Fallback" />}>
|
||||
<Component key="Unrelated at Start" />
|
||||
<React.Suspense
|
||||
name="one"
|
||||
fallback={<Component key="Suspense 1 Fallback" />}>
|
||||
<Component key="Suspense 1 Content" />
|
||||
</React.Suspense>
|
||||
<React.Suspense
|
||||
name="two"
|
||||
fallback={<Component key="Suspense 2 Fallback" />}>
|
||||
<Component key="Suspense 2 Content" />
|
||||
</React.Suspense>
|
||||
<React.Suspense
|
||||
name="three"
|
||||
fallback={<Component key="Suspense 3 Fallback" />}>
|
||||
<Component key="Suspense 3 Content" />
|
||||
</React.Suspense>
|
||||
<Component key="Unrelated at End" />
|
||||
</React.Suspense>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
await actAsync(() => render(<App />));
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Component key="Outside">
|
||||
▾ <Suspense name="parent">
|
||||
<Component key="Unrelated at Start">
|
||||
▾ <Suspense name="one">
|
||||
<Component key="Suspense 1 Content">
|
||||
▾ <Suspense name="two">
|
||||
<Component key="Suspense 2 Content">
|
||||
▾ <Suspense name="three">
|
||||
<Component key="Suspense 3 Content">
|
||||
<Component key="Unrelated at End">
|
||||
[shell]
|
||||
<Suspense name="parent" rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}>
|
||||
<Suspense name="one" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||
<Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||
<Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||
`);
|
||||
|
||||
const rendererID = getRendererID();
|
||||
const rootID = store.getRootIDForElement(store.getElementIDAtIndex(0));
|
||||
await actAsync(() => {
|
||||
agent.overrideSuspenseMilestone({
|
||||
rendererID,
|
||||
rootID,
|
||||
suspendedSet: [
|
||||
store.getElementIDAtIndex(4),
|
||||
store.getElementIDAtIndex(8),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
expect(store).toMatchInlineSnapshot(`
|
||||
[root]
|
||||
▾ <App>
|
||||
<Component key="Outside">
|
||||
▾ <Suspense name="parent">
|
||||
<Component key="Unrelated at Start">
|
||||
▾ <Suspense name="one">
|
||||
<Component key="Suspense 1 Fallback">
|
||||
▾ <Suspense name="two">
|
||||
<Component key="Suspense 2 Content">
|
||||
▾ <Suspense name="three">
|
||||
<Component key="Suspense 3 Fallback">
|
||||
<Component key="Unrelated at End">
|
||||
[shell]
|
||||
<Suspense name="parent" rects={[{x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}, {x:1,y:2,width:5,height:1}]}>
|
||||
<Suspense name="one" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||
<Suspense name="two" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||
<Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should display a partially rendered SuspenseList', async () => {
|
||||
const Loading = () => <div>Loading...</div>;
|
||||
const SuspendingComponent = () => {
|
||||
|
||||
@@ -130,6 +130,12 @@ type OverrideSuspenseParams = {
|
||||
forceFallback: boolean,
|
||||
};
|
||||
|
||||
type OverrideSuspenseMilestoneParams = {
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
suspendedSet: Array<number>,
|
||||
};
|
||||
|
||||
type PersistedSelection = {
|
||||
rendererID: number,
|
||||
path: Array<PathFrame>,
|
||||
@@ -198,6 +204,10 @@ export default class Agent extends EventEmitter<{
|
||||
bridge.addListener('logElementToConsole', this.logElementToConsole);
|
||||
bridge.addListener('overrideError', this.overrideError);
|
||||
bridge.addListener('overrideSuspense', this.overrideSuspense);
|
||||
bridge.addListener(
|
||||
'overrideSuspenseMilestone',
|
||||
this.overrideSuspenseMilestone,
|
||||
);
|
||||
bridge.addListener('overrideValueAtPath', this.overrideValueAtPath);
|
||||
bridge.addListener('reloadAndProfile', this.reloadAndProfile);
|
||||
bridge.addListener('renamePath', this.renamePath);
|
||||
@@ -556,6 +566,21 @@ export default class Agent extends EventEmitter<{
|
||||
}
|
||||
};
|
||||
|
||||
overrideSuspenseMilestone: OverrideSuspenseMilestoneParams => void = ({
|
||||
rendererID,
|
||||
rootID,
|
||||
suspendedSet,
|
||||
}) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(
|
||||
`Invalid renderer id "${rendererID}" to override suspense milestone`,
|
||||
);
|
||||
} else {
|
||||
renderer.overrideSuspenseMilestone(rootID, suspendedSet);
|
||||
}
|
||||
};
|
||||
|
||||
overrideValueAtPath: OverrideValueAtPathParams => void = ({
|
||||
hookID,
|
||||
id,
|
||||
|
||||
@@ -2366,6 +2366,7 @@ export function attach(
|
||||
!isProductionBuildOfRenderer && StrictModeBits !== 0 ? 1 : 0,
|
||||
);
|
||||
pushOperation(hasOwnerMetadata ? 1 : 0);
|
||||
pushOperation(supportsTogglingSuspense ? 1 : 0);
|
||||
|
||||
if (isProfiling) {
|
||||
if (displayNamesByRootID !== null) {
|
||||
@@ -7455,13 +7456,6 @@ export function attach(
|
||||
}
|
||||
|
||||
function overrideSuspense(id: number, forceFallback: boolean) {
|
||||
if (!supportsTogglingSuspense) {
|
||||
// TODO:: Add getter to decide if overrideSuspense is available.
|
||||
// Currently only available on inspectElement.
|
||||
// Probably need a different affordance to batch since the timeline
|
||||
// fallback is not the same as resuspending.
|
||||
return;
|
||||
}
|
||||
if (
|
||||
typeof setSuspenseHandler !== 'function' ||
|
||||
typeof scheduleUpdate !== 'function'
|
||||
@@ -7506,6 +7500,58 @@ export function attach(
|
||||
scheduleUpdate(fiber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the all other roots of this renderer.
|
||||
* @param rootID The root that contains this milestone
|
||||
* @param suspendedSet List of IDs of SuspenseComponent Fibers
|
||||
*/
|
||||
function overrideSuspenseMilestone(
|
||||
rootID: FiberInstance['id'],
|
||||
suspendedSet: Array<FiberInstance['id']>,
|
||||
) {
|
||||
if (
|
||||
typeof setSuspenseHandler !== 'function' ||
|
||||
typeof scheduleUpdate !== 'function'
|
||||
) {
|
||||
throw new Error(
|
||||
'Expected overrideSuspenseMilestone() to not get called for earlier React versions.',
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Allow overriding the timeline for the specified root.
|
||||
forceFallbackForFibers.clear();
|
||||
|
||||
for (let i = 0; i < suspendedSet.length; ++i) {
|
||||
const instance = idToDevToolsInstanceMap.get(suspendedSet[i]);
|
||||
if (instance === undefined) {
|
||||
console.warn(
|
||||
`Could not suspend ID '${suspendedSet[i]}' since the instance can't be found.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (instance.kind === FIBER_INSTANCE) {
|
||||
const fiber = instance.data;
|
||||
forceFallbackForFibers.add(fiber);
|
||||
// We could find a minimal set that covers all the Fibers in this suspended set.
|
||||
// For now we rely on React's batching of updates.
|
||||
scheduleUpdate(fiber);
|
||||
} else {
|
||||
console.warn(`Cannot not suspend ID '${suspendedSet[i]}'.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (forceFallbackForFibers.size > 0) {
|
||||
// First override is added. Switch React to slower path.
|
||||
// TODO: Semantics for suspending a timeline are different. We want a suspended
|
||||
// timeline to act like a first reveal which is relevant for SuspenseList.
|
||||
// Resuspending would not affect rows in SuspenseList
|
||||
setSuspenseHandler(shouldSuspendFiberAccordingToSet);
|
||||
} else {
|
||||
setSuspenseHandler(shouldSuspendFiberAlwaysFalse);
|
||||
}
|
||||
}
|
||||
|
||||
// Remember if we're trying to restore the selection after reload.
|
||||
// In that case, we'll do some extra checks for matching mounts.
|
||||
let trackedPath: Array<PathFrame> | null = null;
|
||||
@@ -8006,6 +8052,7 @@ export function attach(
|
||||
onErrorOrWarning,
|
||||
overrideError,
|
||||
overrideSuspense,
|
||||
overrideSuspenseMilestone,
|
||||
overrideValueAtPath,
|
||||
renamePath,
|
||||
renderer,
|
||||
@@ -8014,6 +8061,7 @@ export function attach(
|
||||
startProfiling,
|
||||
stopProfiling,
|
||||
storeAsGlobal,
|
||||
supportsTogglingSuspense,
|
||||
updateComponentFilters,
|
||||
getEnvironmentNames,
|
||||
...internalMcpFunctions,
|
||||
|
||||
@@ -140,6 +140,8 @@ export function attach(
|
||||
// The changes will be flushed later when we commit this tree to Fiber.
|
||||
}
|
||||
|
||||
const supportsTogglingSuspense = false;
|
||||
|
||||
return {
|
||||
cleanup() {},
|
||||
clearErrorsAndWarnings() {},
|
||||
@@ -202,6 +204,7 @@ export function attach(
|
||||
onErrorOrWarning,
|
||||
overrideError() {},
|
||||
overrideSuspense() {},
|
||||
overrideSuspenseMilestone() {},
|
||||
overrideValueAtPath() {},
|
||||
renamePath() {},
|
||||
renderer,
|
||||
@@ -210,6 +213,7 @@ export function attach(
|
||||
startProfiling() {},
|
||||
stopProfiling() {},
|
||||
storeAsGlobal() {},
|
||||
supportsTogglingSuspense,
|
||||
updateComponentFilters() {},
|
||||
getEnvironmentNames() {
|
||||
return [];
|
||||
|
||||
@@ -180,6 +180,8 @@ export function attach(
|
||||
};
|
||||
}
|
||||
|
||||
const supportsTogglingSuspense = false;
|
||||
|
||||
function getDisplayNameForElementID(id: number): string | null {
|
||||
const internalInstance = idToInternalInstanceMap.get(id);
|
||||
return internalInstance ? getData(internalInstance).displayName : null;
|
||||
@@ -408,6 +410,7 @@ export function attach(
|
||||
pushOperation(0); // Profiling flag
|
||||
pushOperation(0); // StrictMode supported?
|
||||
pushOperation(hasOwnerMetadata ? 1 : 0);
|
||||
pushOperation(supportsTogglingSuspense ? 1 : 0);
|
||||
} else {
|
||||
const type = getElementType(internalInstance);
|
||||
const {displayName, key} = getData(internalInstance);
|
||||
@@ -1070,6 +1073,9 @@ export function attach(
|
||||
const overrideSuspense = () => {
|
||||
throw new Error('overrideSuspense not supported by this renderer');
|
||||
};
|
||||
const overrideSuspenseMilestone = () => {
|
||||
throw new Error('overrideSuspenseMilestone not supported by this renderer');
|
||||
};
|
||||
const startProfiling = () => {
|
||||
// Do not throw, since this would break a multi-root scenario where v15 and v16 were both present.
|
||||
};
|
||||
@@ -1153,6 +1159,7 @@ export function attach(
|
||||
logElementToConsole,
|
||||
overrideError,
|
||||
overrideSuspense,
|
||||
overrideSuspenseMilestone,
|
||||
overrideValueAtPath,
|
||||
renamePath,
|
||||
getElementAttributeByPath,
|
||||
@@ -1163,6 +1170,7 @@ export function attach(
|
||||
startProfiling,
|
||||
stopProfiling,
|
||||
storeAsGlobal,
|
||||
supportsTogglingSuspense,
|
||||
updateComponentFilters,
|
||||
getEnvironmentNames,
|
||||
};
|
||||
|
||||
@@ -437,6 +437,10 @@ export type RendererInterface = {
|
||||
onErrorOrWarning?: OnErrorOrWarning,
|
||||
overrideError: (id: number, forceError: boolean) => void,
|
||||
overrideSuspense: (id: number, forceFallback: boolean) => void,
|
||||
overrideSuspenseMilestone: (
|
||||
rootID: number,
|
||||
suspendedSet: Array<number>,
|
||||
) => void,
|
||||
overrideValueAtPath: (
|
||||
type: Type,
|
||||
id: number,
|
||||
@@ -469,6 +473,7 @@ export type RendererInterface = {
|
||||
path: Array<string | number>,
|
||||
count: number,
|
||||
) => void,
|
||||
supportsTogglingSuspense: boolean,
|
||||
updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void,
|
||||
getEnvironmentNames: () => Array<string>,
|
||||
|
||||
|
||||
15
packages/react-devtools-shared/src/bridge.js
vendored
15
packages/react-devtools-shared/src/bridge.js
vendored
@@ -27,7 +27,7 @@ export type BridgeProtocol = {
|
||||
// Version supported by the current frontend/backend.
|
||||
version: number,
|
||||
|
||||
// NPM version range that also supports this version.
|
||||
// NPM version range of `react-devtools-inline` that also supports this version.
|
||||
// Note that 'maxNpmVersion' is only set when the version is bumped.
|
||||
minNpmVersion: string,
|
||||
maxNpmVersion: string | null,
|
||||
@@ -65,6 +65,12 @@ export const BRIDGE_PROTOCOL: Array<BridgeProtocol> = [
|
||||
{
|
||||
version: 2,
|
||||
minNpmVersion: '4.22.0',
|
||||
maxNpmVersion: '6.2.0',
|
||||
},
|
||||
// Version 3 adds supports-toggling-suspense bit to add-root
|
||||
{
|
||||
version: 3,
|
||||
minNpmVersion: '6.2.0',
|
||||
maxNpmVersion: null,
|
||||
},
|
||||
];
|
||||
@@ -134,6 +140,12 @@ type OverrideSuspense = {
|
||||
forceFallback: boolean,
|
||||
};
|
||||
|
||||
type OverrideSuspenseMilestone = {
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
suspendedSet: Array<number>,
|
||||
};
|
||||
|
||||
type CopyElementPathParams = {
|
||||
...ElementAndRendererID,
|
||||
path: Array<string | number>,
|
||||
@@ -231,6 +243,7 @@ type FrontendEvents = {
|
||||
logElementToConsole: [ElementAndRendererID],
|
||||
overrideError: [OverrideError],
|
||||
overrideSuspense: [OverrideSuspense],
|
||||
overrideSuspenseMilestone: [OverrideSuspenseMilestone],
|
||||
overrideValueAtPath: [OverrideValueAtPath],
|
||||
profilingData: [ProfilingDataBackend],
|
||||
reloadAndProfile: [ReloadAndProfilingParams],
|
||||
|
||||
@@ -89,6 +89,7 @@ export type Capabilities = {
|
||||
supportsBasicProfiling: boolean,
|
||||
hasOwnerMetadata: boolean,
|
||||
supportsStrictMode: boolean,
|
||||
supportsTogglingSuspense: boolean,
|
||||
supportsTimeline: boolean,
|
||||
};
|
||||
|
||||
@@ -491,6 +492,14 @@ export default class Store extends EventEmitter<{
|
||||
);
|
||||
}
|
||||
|
||||
supportsTogglingSuspense(rootID: Element['id']): boolean {
|
||||
const capabilities = this._rootIDToCapabilities.get(rootID);
|
||||
if (capabilities === undefined) {
|
||||
throw new Error(`No capabilities registered for root ${rootID}`);
|
||||
}
|
||||
return capabilities.supportsTogglingSuspense;
|
||||
}
|
||||
|
||||
// This build of DevTools supports the Timeline profiler.
|
||||
// This is a static flag, controlled by the Store config.
|
||||
get supportsTimeline(): boolean {
|
||||
@@ -1080,6 +1089,7 @@ export default class Store extends EventEmitter<{
|
||||
|
||||
let supportsStrictMode = false;
|
||||
let hasOwnerMetadata = false;
|
||||
let supportsTogglingSuspense = false;
|
||||
|
||||
// If we don't know the bridge protocol, guess that we're dealing with the latest.
|
||||
// If we do know it, we can take it into consideration when parsing operations.
|
||||
@@ -1092,6 +1102,9 @@ export default class Store extends EventEmitter<{
|
||||
|
||||
hasOwnerMetadata = operations[i] > 0;
|
||||
i++;
|
||||
|
||||
supportsTogglingSuspense = operations[i] > 0;
|
||||
i++;
|
||||
}
|
||||
|
||||
this._roots = this._roots.concat(id);
|
||||
@@ -1100,6 +1113,7 @@ export default class Store extends EventEmitter<{
|
||||
supportsBasicProfiling,
|
||||
hasOwnerMetadata,
|
||||
supportsStrictMode,
|
||||
supportsTogglingSuspense,
|
||||
supportsTimeline,
|
||||
});
|
||||
|
||||
|
||||
@@ -208,6 +208,7 @@ function updateTree(
|
||||
i++; // Profiling flag
|
||||
i++; // supportsStrictMode flag
|
||||
i++; // hasOwnerMetadata flag
|
||||
i++; // supportsTogglingSuspense flag
|
||||
|
||||
if (__DEBUG__) {
|
||||
debug('Add', `new root fiber ${id}`);
|
||||
|
||||
@@ -115,4 +115,5 @@
|
||||
|
||||
.Timeline {
|
||||
flex-grow: 1;
|
||||
align-self: anchor-center;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
.SuspenseTimelineSlider {
|
||||
.SuspenseTimelineContainer {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.SuspenseTimelineInput {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.SuspenseTimelineRootSwitcher {
|
||||
height: fit-content;
|
||||
max-width: 3rem;
|
||||
}
|
||||
|
||||
.SuspenseTimelineMarkers {
|
||||
@@ -18,3 +31,4 @@
|
||||
.SuspenseTimelineActiveMarker {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,34 +29,38 @@ import typeof {
|
||||
SyntheticPointerEvent,
|
||||
} from 'react-dom-bindings/src/events/SyntheticEvent';
|
||||
|
||||
// TODO: This returns the roots which would mean we attempt to suspend the shell.
|
||||
// Suspending the shell is currently not supported and we don't have a good view
|
||||
// for inspecting the root. But we probably should?
|
||||
function getDocumentOrderSuspense(
|
||||
function getSuspendableDocumentOrderSuspense(
|
||||
store: Store,
|
||||
roots: $ReadOnlyArray<Element['id']>,
|
||||
rootID: Element['id'] | void,
|
||||
): Array<SuspenseNode> {
|
||||
if (rootID === undefined) {
|
||||
return [];
|
||||
}
|
||||
const root = store.getElementByID(rootID);
|
||||
if (root === null) {
|
||||
return [];
|
||||
}
|
||||
if (!store.supportsTogglingSuspense(root.id)) {
|
||||
return [];
|
||||
}
|
||||
const suspenseTreeList: SuspenseNode[] = [];
|
||||
for (let i = 0; i < roots.length; i++) {
|
||||
const root = store.getElementByID(roots[i]);
|
||||
if (root === null) {
|
||||
continue;
|
||||
}
|
||||
const suspense = store.getSuspenseByID(root.id);
|
||||
if (suspense !== null) {
|
||||
const stack = [suspense];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
if (current === undefined) {
|
||||
continue;
|
||||
}
|
||||
const suspense = store.getSuspenseByID(root.id);
|
||||
if (suspense !== null) {
|
||||
const stack = [suspense];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
if (current === undefined) {
|
||||
continue;
|
||||
}
|
||||
// Don't include the root. It's currently not supported to suspend the shell.
|
||||
if (current !== suspense) {
|
||||
suspenseTreeList.push(current);
|
||||
// Add children in reverse order to maintain document order
|
||||
for (let j = current.children.length - 1; j >= 0; j--) {
|
||||
const childSuspense = store.getSuspenseByID(current.children[j]);
|
||||
if (childSuspense !== null) {
|
||||
stack.push(childSuspense);
|
||||
}
|
||||
}
|
||||
// Add children in reverse order to maintain document order
|
||||
for (let j = current.children.length - 1; j >= 0; j--) {
|
||||
const childSuspense = store.getSuspenseByID(current.children[j]);
|
||||
if (childSuspense !== null) {
|
||||
stack.push(childSuspense);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,22 +69,24 @@ function getDocumentOrderSuspense(
|
||||
return suspenseTreeList;
|
||||
}
|
||||
|
||||
export default function SuspenseTimeline(): React$Node {
|
||||
function SuspenseTimelineInput({rootID}: {rootID: Element['id'] | void}) {
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
const {shells} = useContext(SuspenseTreeStateContext);
|
||||
|
||||
const timeline = useMemo(() => {
|
||||
return getDocumentOrderSuspense(store, shells);
|
||||
}, [store, shells]);
|
||||
|
||||
const {highlightHostInstance, clearHighlightHostInstance} =
|
||||
useHighlightHostInstance();
|
||||
|
||||
const timeline = useMemo(() => {
|
||||
return getSuspendableDocumentOrderSuspense(store, rootID);
|
||||
}, [store, rootID]);
|
||||
|
||||
const inputRef = useRef<HTMLElement | null>(null);
|
||||
const inputBBox = useRef<ClientRect | null>(null);
|
||||
useLayoutEffect(() => {
|
||||
if (timeline.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const input = inputRef.current;
|
||||
if (input === null) {
|
||||
throw new Error('Expected an input HTML element to be present.');
|
||||
@@ -95,12 +101,12 @@ export default function SuspenseTimeline(): React$Node {
|
||||
inputBBox.current = null;
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
}, [timeline.length]);
|
||||
|
||||
const min = 0;
|
||||
const max = timeline.length > 0 ? timeline.length - 1 : 0;
|
||||
|
||||
const [value, setValue] = useState(max);
|
||||
|
||||
if (value > max) {
|
||||
// TODO: Handle timeline changes
|
||||
setValue(max);
|
||||
@@ -130,6 +136,26 @@ export default function SuspenseTimeline(): React$Node {
|
||||
});
|
||||
}, [timeline, value]);
|
||||
|
||||
if (rootID === undefined) {
|
||||
return <div className={styles.SuspenseTimelineInput}>Root not found.</div>;
|
||||
}
|
||||
|
||||
if (!store.supportsTogglingSuspense(rootID)) {
|
||||
return (
|
||||
<div className={styles.SuspenseTimelineInput}>
|
||||
Can't step through Suspense in production apps.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (timeline.length === 0) {
|
||||
return (
|
||||
<div className={styles.SuspenseTimelineInput}>
|
||||
Root contains no Suspense nodes.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function handleChange(event: SyntheticEvent) {
|
||||
const pendingValue = +event.currentTarget.value;
|
||||
for (let i = 0; i < timeline.length; i++) {
|
||||
@@ -193,7 +219,7 @@ export default function SuspenseTimeline(): React$Node {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.SuspenseTimelineInput}>
|
||||
<input
|
||||
className={styles.SuspenseTimelineSlider}
|
||||
type="range"
|
||||
@@ -214,3 +240,54 @@ export default function SuspenseTimeline(): React$Node {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SuspenseTimeline(): React$Node {
|
||||
const store = useContext(StoreContext);
|
||||
const {shells} = useContext(SuspenseTreeStateContext);
|
||||
|
||||
const defaultSelectedRootID = shells.find(rootID => {
|
||||
const suspense = store.getSuspenseByID(rootID);
|
||||
return (
|
||||
store.supportsTogglingSuspense(rootID) &&
|
||||
suspense !== null &&
|
||||
suspense.children.length > 1
|
||||
);
|
||||
});
|
||||
const [selectedRootID, setSelectedRootID] = useState(defaultSelectedRootID);
|
||||
|
||||
if (selectedRootID === undefined && defaultSelectedRootID !== undefined) {
|
||||
setSelectedRootID(defaultSelectedRootID);
|
||||
}
|
||||
|
||||
function handleChange(event: SyntheticEvent) {
|
||||
const newRootID = +event.currentTarget.value;
|
||||
// TODO: scrollIntoView both suspense rects and host instance.
|
||||
setSelectedRootID(newRootID);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.SuspenseTimelineContainer}>
|
||||
<SuspenseTimelineInput key={selectedRootID} rootID={selectedRootID} />
|
||||
{shells.length > 0 && (
|
||||
<select
|
||||
aria-label="Select Suspense Root"
|
||||
className={styles.SuspenseTimelineRootSwitcher}
|
||||
onChange={handleChange}>
|
||||
{shells.map(rootID => {
|
||||
// TODO: Use name
|
||||
const name = '#' + rootID;
|
||||
// TODO: Highlight host on hover
|
||||
return (
|
||||
<option
|
||||
key={rootID}
|
||||
selected={rootID === selectedRootID}
|
||||
value={rootID}>
|
||||
{name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
1
packages/react-devtools-shared/src/utils.js
vendored
1
packages/react-devtools-shared/src/utils.js
vendored
@@ -261,6 +261,7 @@ export function printOperationsArray(operations: Array<number>) {
|
||||
i++; // supportsProfiling
|
||||
i++; // supportsStrictMode
|
||||
i++; // hasOwnerMetadata
|
||||
i++; // supportsTogglingSuspense
|
||||
} else {
|
||||
const parentID = ((operations[i]: any): number);
|
||||
i++;
|
||||
|
||||
Reference in New Issue
Block a user