Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions packages/react-devtools-shared/src/__tests__/store-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
25 changes: 25 additions & 0 deletions packages/react-devtools-shared/src/backend/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ type OverrideSuspenseParams = {
forceFallback: boolean,
};

type OverrideSuspenseMilestoneParams = {
rendererID: number,
rootID: number,
suspendedSet: Array<number>,
};

type PersistedSelection = {
rendererID: number,
path: Array<PathFrame>,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
62 changes: 55 additions & 7 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -8006,6 +8052,7 @@ export function attach(
onErrorOrWarning,
overrideError,
overrideSuspense,
overrideSuspenseMilestone,
overrideValueAtPath,
renamePath,
renderer,
Expand All @@ -8014,6 +8061,7 @@ export function attach(
startProfiling,
stopProfiling,
storeAsGlobal,
supportsTogglingSuspense,
updateComponentFilters,
getEnvironmentNames,
...internalMcpFunctions,
Expand Down
4 changes: 4 additions & 0 deletions packages/react-devtools-shared/src/backend/flight/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {},
Expand Down Expand Up @@ -202,6 +204,7 @@ export function attach(
onErrorOrWarning,
overrideError() {},
overrideSuspense() {},
overrideSuspenseMilestone() {},
overrideValueAtPath() {},
renamePath() {},
renderer,
Expand All @@ -210,6 +213,7 @@ export function attach(
startProfiling() {},
stopProfiling() {},
storeAsGlobal() {},
supportsTogglingSuspense,
updateComponentFilters() {},
getEnvironmentNames() {
return [];
Expand Down
8 changes: 8 additions & 0 deletions packages/react-devtools-shared/src/backend/legacy/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
};
Expand Down Expand Up @@ -1153,6 +1159,7 @@ export function attach(
logElementToConsole,
overrideError,
overrideSuspense,
overrideSuspenseMilestone,
overrideValueAtPath,
renamePath,
getElementAttributeByPath,
Expand All @@ -1163,6 +1170,7 @@ export function attach(
startProfiling,
stopProfiling,
storeAsGlobal,
supportsTogglingSuspense,
updateComponentFilters,
getEnvironmentNames,
};
Expand Down
5 changes: 5 additions & 0 deletions packages/react-devtools-shared/src/backend/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -469,6 +473,7 @@ export type RendererInterface = {
path: Array<string | number>,
count: number,
) => void,
supportsTogglingSuspense: boolean,
updateComponentFilters: (componentFilters: Array<ComponentFilter>) => void,
getEnvironmentNames: () => Array<string>,

Expand Down
15 changes: 14 additions & 1 deletion packages/react-devtools-shared/src/bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
},
];
Expand Down Expand Up @@ -134,6 +140,12 @@ type OverrideSuspense = {
forceFallback: boolean,
};

type OverrideSuspenseMilestone = {
rendererID: number,
rootID: number,
suspendedSet: Array<number>,
};

type CopyElementPathParams = {
...ElementAndRendererID,
path: Array<string | number>,
Expand Down Expand Up @@ -231,6 +243,7 @@ type FrontendEvents = {
logElementToConsole: [ElementAndRendererID],
overrideError: [OverrideError],
overrideSuspense: [OverrideSuspense],
overrideSuspenseMilestone: [OverrideSuspenseMilestone],
overrideValueAtPath: [OverrideValueAtPath],
profilingData: [ProfilingDataBackend],
reloadAndProfile: [ReloadAndProfilingParams],
Expand Down
Loading
Loading