Merge 38e718ed68 into sapling-pr-archive-poteto

This commit is contained in:
lauren
2025-08-12 20:29:10 -04:00
committed by GitHub
46 changed files with 2594 additions and 592 deletions
+9 -9
View File
@@ -2822,7 +2822,7 @@ describe('ReactFlight', () => {
expect(getDebugInfo(promise)).toEqual(
__DEV__
? [
{time: 20},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 22 : 20},
{
name: 'ServerComponent',
env: 'Server',
@@ -2832,7 +2832,7 @@ describe('ReactFlight', () => {
transport: expect.arrayContaining([]),
},
},
{time: 21},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 23 : 21},
]
: undefined,
);
@@ -2843,7 +2843,7 @@ describe('ReactFlight', () => {
expect(getDebugInfo(thirdPartyChildren[0])).toEqual(
__DEV__
? [
{time: 22}, // Clamped to the start
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start
{
name: 'ThirdPartyComponent',
env: 'third-party',
@@ -2851,15 +2851,15 @@ describe('ReactFlight', () => {
stack: ' in Object.<anonymous> (at **)',
props: {},
},
{time: 22},
{time: 23}, // This last one is when the promise resolved into the first party.
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 25 : 23}, // This last one is when the promise resolved into the first party.
]
: undefined,
);
expect(getDebugInfo(thirdPartyChildren[1])).toEqual(
__DEV__
? [
{time: 22}, // Clamped to the start
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22}, // Clamped to the start
{
name: 'ThirdPartyLazyComponent',
env: 'third-party',
@@ -2867,14 +2867,14 @@ describe('ReactFlight', () => {
stack: ' in myLazy (at **)\n in lazyInitializer (at **)',
props: {},
},
{time: 22},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
]
: undefined,
);
expect(getDebugInfo(thirdPartyChildren[2])).toEqual(
__DEV__
? [
{time: 22},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
{
name: 'ThirdPartyFragmentComponent',
env: 'third-party',
@@ -2882,7 +2882,7 @@ describe('ReactFlight', () => {
stack: ' in Object.<anonymous> (at **)',
props: {},
},
{time: 22},
{time: gate(flags => flags.enableAsyncDebugInfo) ? 24 : 22},
]
: undefined,
);
+21 -1
View File
@@ -147,6 +147,8 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
let currentFiber: null | Fiber = null;
let currentHook: null | Hook = null;
let currentContextDependency: null | ContextDependency<mixed> = null;
let currentThenableIndex: number = 0;
let currentThenableState: null | Array<Thenable<mixed>> = null;
function nextHook(): null | Hook {
const hook = currentHook;
@@ -201,7 +203,15 @@ function use<T>(usable: Usable<T>): T {
if (usable !== null && typeof usable === 'object') {
// $FlowFixMe[method-unbinding]
if (typeof usable.then === 'function') {
const thenable: Thenable<any> = (usable: any);
const thenable: Thenable<any> =
// If we have thenable state, then the actually used thenable will be the one
// stashed in it. It's possible for uncached Promises to be new each render
// and in that case the one we're inspecting is the in the thenable state.
currentThenableState !== null &&
currentThenableIndex < currentThenableState.length
? currentThenableState[currentThenableIndex++]
: (usable: any);
switch (thenable.status) {
case 'fulfilled': {
const fulfilledValue: T = thenable.value;
@@ -1285,6 +1295,14 @@ export function inspectHooksOfFiber(
// current state from them.
currentHook = (fiber.memoizedState: Hook);
currentFiber = fiber;
const thenableState =
fiber.dependencies && fiber.dependencies._debugThenableState;
// In DEV the thenableState is an inner object.
const usedThenables: any = thenableState
? thenableState.thenables || thenableState
: null;
currentThenableState = Array.isArray(usedThenables) ? usedThenables : null;
currentThenableIndex = 0;
if (hasOwnProperty.call(currentFiber, 'dependencies')) {
// $FlowFixMe[incompatible-use]: Flow thinks hasOwnProperty might have nulled `currentFiber`
@@ -1339,6 +1357,8 @@ export function inspectHooksOfFiber(
currentFiber = null;
currentHook = null;
currentContextDependency = null;
currentThenableState = null;
currentThenableIndex = 0;
restoreContexts(contextMap);
}
@@ -64,11 +64,22 @@ async function selectElement(
createTestNameSelector('InspectedElementView-Owners'),
])[0];
if (!ownersList) {
return false;
}
const owners = findAllNodes(ownersList, [
createTestNameSelector('OwnerView'),
]);
return (
title &&
title.innerText.includes(titleText) &&
ownersList &&
ownersList.innerText.includes(ownersListText)
owners &&
owners
.map(node => node.innerText)
.join('\n')
.includes(ownersListText)
);
},
{titleText: displayName, ownersListText: waitForOwnersText}
@@ -682,6 +682,7 @@ describe('InspectedElement', () => {
object_with_symbol={objectWithSymbol}
proxy={proxyInstance}
react_element={<span />}
react_lazy={React.lazy(async () => ({default: 'foo'}))}
regexp={/abc/giu}
set={setShallow}
set_of_sets={setOfSets}
@@ -780,9 +781,18 @@ describe('InspectedElement', () => {
"preview_short": () => {},
"preview_long": () => {},
},
"react_element": Dehydrated {
"preview_short": <span />,
"preview_long": <span />,
"react_element": {
"key": null,
"props": Dehydrated {
"preview_short": {…},
"preview_long": {},
},
},
"react_lazy": {
"_payload": Dehydrated {
"preview_short": {…},
"preview_long": {_ioInfo: {…}, _result: () => {}, _status: -1},
},
},
"regexp": Dehydrated {
"preview_short": /abc/giu,
@@ -930,13 +940,13 @@ describe('InspectedElement', () => {
const inspectedElement = await inspectElementAtIndex(0);
expect(inspectedElement.props).toMatchInlineSnapshot(`
{
"unusedPromise": Dehydrated {
"preview_short": Promise,
"preview_long": Promise,
},
}
`);
{
"unusedPromise": Dehydrated {
"preview_short": Promise,
"preview_long": Promise,
},
}
`);
});
it('should not consume iterables while inspecting', async () => {
@@ -289,9 +289,13 @@ describe('InspectedElementContext', () => {
"preview_long": {boolean: true, number: 123, string: "abc"},
},
},
"react_element": Dehydrated {
"preview_short": <span />,
"preview_long": <span />,
"react_element": {
"key": null,
"props": Dehydrated {
"preview_short": {…},
"preview_long": {},
},
"ref": null,
},
"regexp": Dehydrated {
"preview_short": /abc/giu,
@@ -949,6 +949,7 @@ describe('ProfilingCache', () => {
"hocDisplayNames": null,
"id": 1,
"key": null,
"stack": null,
"type": 11,
},
],
@@ -228,6 +228,8 @@ describe('commit tree', () => {
[root]
▾ <App>
<Suspense>
[shell]
<Suspense name="App>?" rects={null}>
`);
utils.act(() => modernRender(<App renderChildren={true} />));
expect(store).toMatchInlineSnapshot(`
@@ -235,6 +237,8 @@ describe('commit tree', () => {
▾ <App>
▾ <Suspense>
<LazyInnerComponent>
[shell]
<Suspense name="App>?" rects={null}>
`);
utils.act(() => modernRender(<App renderChildren={false} />));
expect(store).toMatchInlineSnapshot(`
@@ -299,6 +303,8 @@ describe('commit tree', () => {
[root]
▾ <App>
<Suspense>
[shell]
<Suspense name="App>?" rects={null}>
`);
utils.act(() => modernRender(<App renderChildren={false} />));
expect(store).toMatchInlineSnapshot(`
+173 -58
View File
@@ -24,6 +24,16 @@ describe('Store', () => {
let store;
let withErrorsOrWarningsIgnored;
beforeAll(() => {
// JSDDOM doesn't implement getClientRects so we're just faking one for testing purposes
Element.prototype.getClientRects = function (this: Element) {
const textContent = this.textContent;
return [
new DOMRect(1, 2, textContent.length, textContent.split('\n').length),
];
};
});
beforeEach(() => {
global.IS_REACT_ACT_ENVIRONMENT = true;
@@ -123,6 +133,8 @@ describe('Store', () => {
<Suspense>
▾ <Parent>
<Child>
[shell]
<Suspense name="Unknown" rects={null}>
`);
});
@@ -480,6 +492,8 @@ describe('Store', () => {
<Component key="Outside">
▾ <Suspense>
<Loading>
[shell]
<Suspense name="Wrapper>?" rects={null}>
`);
await act(() => {
@@ -491,6 +505,8 @@ describe('Store', () => {
<Component key="Outside">
▾ <Suspense>
<Component key="Inside">
[shell]
<Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
`);
});
@@ -513,23 +529,31 @@ describe('Store', () => {
}) => (
<React.Fragment>
<Component key="Outside" />
<React.Suspense fallback={<Loading key="Parent Fallback" />}>
<React.Suspense
name="parent"
fallback={<Loading key="Parent Fallback" />}>
<Component key="Unrelated at Start" />
<React.Suspense fallback={<Loading key="Suspense 1 Fallback" />}>
<React.Suspense
name="one"
fallback={<Loading key="Suspense 1 Fallback" />}>
{suspendFirst ? (
<Never />
) : (
<Component key="Suspense 1 Content" />
)}
</React.Suspense>
<React.Suspense fallback={<Loading key="Suspense 2 Fallback" />}>
<React.Suspense
name="two"
fallback={<Loading key="Suspense 2 Fallback" />}>
{suspendSecond ? (
<Never />
) : (
<Component key="Suspense 2 Content" />
)}
</React.Suspense>
<React.Suspense fallback={<Loading key="Suspense 3 Fallback" />}>
<React.Suspense
name="three"
fallback={<Loading key="Suspense 3 Fallback" />}>
<Never />
</React.Suspense>
{suspendParent && <Never />}
@@ -538,7 +562,7 @@ describe('Store', () => {
</React.Fragment>
);
await act(() =>
await actAsync(() =>
render(
<Wrapper
suspendParent={false}
@@ -551,15 +575,20 @@ describe('Store', () => {
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
▾ <Suspense name="parent">
<Component key="Unrelated at Start">
▾ <Suspense>
▾ <Suspense name="one">
<Component key="Suspense 1 Content">
▾ <Suspense>
▾ <Suspense name="two">
<Component key="Suspense 2 Content">
▾ <Suspense>
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
render(
@@ -574,15 +603,20 @@ describe('Store', () => {
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
▾ <Suspense name="parent">
<Component key="Unrelated at Start">
▾ <Suspense>
▾ <Suspense name="one">
<Loading key="Suspense 1 Fallback">
▾ <Suspense>
▾ <Suspense name="two">
<Component key="Suspense 2 Content">
▾ <Suspense>
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
render(
@@ -597,15 +631,20 @@ describe('Store', () => {
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
▾ <Suspense name="parent">
<Component key="Unrelated at Start">
▾ <Suspense>
▾ <Suspense name="one">
<Component key="Suspense 1 Content">
▾ <Suspense>
▾ <Suspense name="two">
<Loading key="Suspense 2 Fallback">
▾ <Suspense>
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
render(
@@ -620,15 +659,20 @@ describe('Store', () => {
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
▾ <Suspense name="parent">
<Component key="Unrelated at Start">
▾ <Suspense>
▾ <Suspense name="one">
<Loading key="Suspense 1 Fallback">
▾ <Suspense>
▾ <Suspense name="two">
<Component key="Suspense 2 Content">
▾ <Suspense>
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
render(
@@ -643,8 +687,13 @@ describe('Store', () => {
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
▾ <Suspense name="parent">
<Loading key="Parent Fallback">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
render(
@@ -659,15 +708,20 @@ describe('Store', () => {
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
▾ <Suspense name="parent">
<Component key="Unrelated at Start">
▾ <Suspense>
▾ <Suspense name="one">
<Loading key="Suspense 1 Fallback">
▾ <Suspense>
▾ <Suspense name="two">
<Loading key="Suspense 2 Fallback">
▾ <Suspense>
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
render(
@@ -682,15 +736,20 @@ describe('Store', () => {
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
▾ <Suspense name="parent">
<Component key="Unrelated at Start">
▾ <Suspense>
▾ <Suspense name="one">
<Component key="Suspense 1 Content">
▾ <Suspense>
▾ <Suspense name="two">
<Component key="Suspense 2 Content">
▾ <Suspense>
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
const rendererID = getRendererID();
@@ -705,15 +764,20 @@ describe('Store', () => {
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
▾ <Suspense name="parent">
<Component key="Unrelated at Start">
▾ <Suspense>
▾ <Suspense name="one">
<Loading key="Suspense 1 Fallback">
▾ <Suspense>
▾ <Suspense name="two">
<Component key="Suspense 2 Content">
▾ <Suspense>
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
agent.overrideSuspense({
@@ -726,8 +790,13 @@ describe('Store', () => {
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
▾ <Suspense name="parent">
<Loading key="Parent Fallback">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
render(
@@ -742,8 +811,13 @@ describe('Store', () => {
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
▾ <Suspense name="parent">
<Loading key="Parent Fallback">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
agent.overrideSuspense({
@@ -756,15 +830,20 @@ describe('Store', () => {
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
▾ <Suspense name="parent">
<Component key="Unrelated at Start">
▾ <Suspense>
▾ <Suspense name="one">
<Loading key="Suspense 1 Fallback">
▾ <Suspense>
▾ <Suspense name="two">
<Loading key="Suspense 2 Fallback">
▾ <Suspense>
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
agent.overrideSuspense({
@@ -777,15 +856,20 @@ describe('Store', () => {
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
▾ <Suspense name="parent">
<Component key="Unrelated at Start">
▾ <Suspense>
▾ <Suspense name="one">
<Loading key="Suspense 1 Fallback">
▾ <Suspense>
▾ <Suspense name="two">
<Loading key="Suspense 2 Fallback">
▾ <Suspense>
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}, {x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
await act(() =>
render(
@@ -800,15 +884,20 @@ describe('Store', () => {
[root]
▾ <Wrapper>
<Component key="Outside">
▾ <Suspense>
▾ <Suspense name="parent">
<Component key="Unrelated at Start">
▾ <Suspense>
▾ <Suspense name="one">
<Component key="Suspense 1 Content">
▾ <Suspense>
▾ <Suspense name="two">
<Component key="Suspense 2 Content">
▾ <Suspense>
▾ <Suspense name="three">
<Loading key="Suspense 3 Fallback">
<Component key="Unrelated at End">
[shell]
<Suspense name="parent" rects={[{x:1,y:2,width:10,height:1}]}>
<Suspense name="one" rects={null}>
<Suspense name="two" rects={null}>
<Suspense name="three" rects={null}>
`);
});
@@ -848,6 +937,8 @@ describe('Store', () => {
<Component key="A">
▾ <Suspense>
<Loading>
[shell]
<Suspense name="Wrapper>?" rects={null}>
`);
await act(() => {
@@ -861,6 +952,8 @@ describe('Store', () => {
▾ <Suspense>
<Component key="B">
<Component key="C">
[shell]
<Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
`);
});
@@ -1197,6 +1290,8 @@ describe('Store', () => {
expect(store).toMatchInlineSnapshot(`
[root]
▸ <Wrapper>
[shell]
<Suspense name="Wrapper>?" rects={null}>
`);
// This test isn't meaningful unless we expand the suspended tree
@@ -1212,6 +1307,8 @@ describe('Store', () => {
<Component key="Outside">
▾ <Suspense>
<Loading>
[shell]
<Suspense name="Wrapper>?" rects={null}>
`);
await act(() => {
@@ -1223,6 +1320,8 @@ describe('Store', () => {
<Component key="Outside">
▾ <Suspense>
<Component key="Inside">
[shell]
<Suspense name="Wrapper>?" rects={[{x:1,y:2,width:5,height:1}]}>
`);
});
@@ -1447,6 +1546,8 @@ describe('Store', () => {
expect(store).toMatchInlineSnapshot(`
[root]
▸ <SuspenseTree>
[shell]
<Suspense name="SuspenseTree>?" rects={null}>
`);
await act(() =>
@@ -1460,6 +1561,8 @@ describe('Store', () => {
▾ <SuspenseTree>
▾ <Suspense>
▸ <Parent>
[shell]
<Suspense name="SuspenseTree>?" rects={null}>
`);
const rendererID = getRendererID();
@@ -1477,6 +1580,8 @@ describe('Store', () => {
▾ <SuspenseTree>
▾ <Suspense>
<Fallback>
[shell]
<Suspense name="SuspenseTree>?" rects={null}>
`);
await act(() =>
@@ -1491,6 +1596,8 @@ describe('Store', () => {
▾ <SuspenseTree>
▾ <Suspense>
▸ <Parent>
[shell]
<Suspense name="SuspenseTree>?" rects={null}>
`);
});
});
@@ -1794,6 +1901,8 @@ describe('Store', () => {
[root]
▾ <App>
<Suspense>
[shell]
<Suspense name="App>?" rects={null}>
`);
await Promise.resolve();
@@ -1806,6 +1915,8 @@ describe('Store', () => {
▾ <App>
▾ <Suspense>
<LazyInnerComponent>
[shell]
<Suspense name="App>?" rects={null}>
`);
// Render again to unmount it
@@ -2291,20 +2402,24 @@ describe('Store', () => {
await actAsync(() => render(<App renderA={true} />));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
▾ <Suspense>
<ChildA>
`);
[root]
▾ <App>
▾ <Suspense>
<ChildA>
[shell]
<Suspense name="App>?" rects={null}>
`);
await actAsync(() => render(<App renderA={false} />));
expect(store).toMatchInlineSnapshot(`
[root]
▾ <App>
▾ <Suspense>
<ChildB>
`);
[root]
▾ <App>
▾ <Suspense>
<ChildB>
[shell]
<Suspense name="App>?" rects={null}>
`);
});
});
@@ -156,6 +156,9 @@ describe('Store component filters', () => {
<div>
▾ <Suspense>
<div>
[shell]
<Suspense name="Unknown" rects={[]}>
<Suspense name="Unknown" rects={[]}>
`);
await actAsync(
@@ -171,6 +174,9 @@ describe('Store component filters', () => {
<div>
▾ <Suspense>
<div>
[shell]
<Suspense name="Unknown" rects={[]}>
<Suspense name="Unknown" rects={[]}>
`);
await actAsync(
@@ -186,6 +192,9 @@ describe('Store component filters', () => {
<div>
▾ <Suspense>
<div>
[shell]
<Suspense name="Unknown" rects={[]}>
<Suspense name="Unknown" rects={[]}>
`);
});
@@ -32,7 +32,7 @@ describe('StoreStressConcurrent', () => {
// this helper with the real thing.
actAsync = require('./utils').actAsync;
print = require('./__serializers__/storeSerializer').print;
print = require('./__serializers__/storeSerializer').printStore;
});
// This is a stress test for the tree mount/update/unmount traversal.
@@ -67,8 +67,7 @@ describe('StoreStressConcurrent', () => {
let container = document.createElement('div');
let root = ReactDOMClient.createRoot(container);
act(() => root.render(<Parent>{[a, b, c, d, e]}</Parent>));
expect(store).toMatchInlineSnapshot(
`
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Parent>
<A key="a">
@@ -76,8 +75,7 @@ describe('StoreStressConcurrent', () => {
<C key="c">
<D key="d">
<E key="e">
`,
);
`);
expect(container.textContent).toMatch('abcde');
const snapshotForABCDE = print(store);
@@ -86,8 +84,7 @@ describe('StoreStressConcurrent', () => {
act(() => {
setShowX(true);
});
expect(store).toMatchInlineSnapshot(
`
expect(store).toMatchInlineSnapshot(`
[root]
▾ <Parent>
<A key="a">
@@ -96,8 +93,7 @@ describe('StoreStressConcurrent', () => {
<X>
<D key="d">
<E key="e">
`,
);
`);
expect(container.textContent).toMatch('abxde');
const snapshotForABXDE = print(store);
@@ -419,7 +415,7 @@ describe('StoreStressConcurrent', () => {
),
);
// We snapshot each step once so it doesn't regress.d
snapshots.push(print(store));
snapshots.push(print(store, false, null, false));
await act(() => root.unmount());
expect(print(store)).toBe('');
}
@@ -524,7 +520,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
await act(() => root.unmount());
expect(print(store)).toBe('');
}
@@ -544,7 +540,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Re-render with steps[j].
await act(() =>
root.render(
@@ -556,7 +552,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Check that we can transition back again.
await act(() =>
root.render(
@@ -567,7 +563,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
expect(print(store)).toBe('');
@@ -593,7 +589,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Re-render with steps[j].
await act(() =>
root.render(
@@ -609,7 +605,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Check that we can transition back again.
await act(() =>
root.render(
@@ -624,7 +620,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
expect(print(store)).toBe('');
@@ -646,7 +642,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Re-render with steps[j].
await act(() =>
root.render(
@@ -662,7 +658,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Check that we can transition back again.
await act(() =>
root.render(
@@ -673,7 +669,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
expect(print(store)).toBe('');
@@ -699,7 +695,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Re-render with steps[j].
await act(() =>
root.render(
@@ -711,7 +707,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Check that we can transition back again.
await act(() =>
root.render(
@@ -726,7 +722,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
expect(print(store)).toBe('');
@@ -755,7 +751,7 @@ describe('StoreStressConcurrent', () => {
const suspenseID = store.getElementIDAtIndex(2);
// Force fallback.
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
await actAsync(async () => {
bridge.send('overrideSuspense', {
id: suspenseID,
@@ -763,7 +759,7 @@ describe('StoreStressConcurrent', () => {
forceFallback: true,
});
});
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Stop forcing fallback.
await actAsync(async () => {
@@ -773,7 +769,7 @@ describe('StoreStressConcurrent', () => {
forceFallback: false,
});
});
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Trigger actual fallback.
await act(() =>
@@ -789,7 +785,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Force fallback while we're in fallback mode.
await act(() => {
@@ -800,7 +796,7 @@ describe('StoreStressConcurrent', () => {
});
});
// Keep seeing fallback content.
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Switch to primary mode.
await act(() =>
@@ -813,7 +809,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Fallback is still forced though.
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Stop forcing fallback. This reverts to primary content.
await actAsync(async () => {
@@ -824,7 +820,7 @@ describe('StoreStressConcurrent', () => {
});
});
// Now we see primary content.
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Clean up after every iteration.
await actAsync(async () => root.unmount());
@@ -910,7 +906,7 @@ describe('StoreStressConcurrent', () => {
),
);
// We snapshot each step once so it doesn't regress.
snapshots.push(print(store));
snapshots.push(print(store, false, null, false));
await act(() => root.unmount());
expect(print(store)).toBe('');
}
@@ -935,7 +931,7 @@ describe('StoreStressConcurrent', () => {
),
);
// We snapshot each step once so it doesn't regress.
fallbackSnapshots.push(print(store));
fallbackSnapshots.push(print(store, false, null, false));
await act(() => root.unmount());
expect(print(store)).toBe('');
}
@@ -1065,7 +1061,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Re-render with steps[j].
await act(() =>
root.render(
@@ -1079,7 +1075,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Check that we can transition back again.
await act(() =>
root.render(
@@ -1092,7 +1088,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
expect(print(store)).toBe('');
@@ -1121,7 +1117,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]);
// Re-render with steps[j].
await act(() =>
root.render(
@@ -1140,7 +1136,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(fallbackSnapshots[j]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
// Check that we can transition back again.
await act(() =>
root.render(
@@ -1158,7 +1154,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
expect(print(store)).toBe('');
@@ -1182,7 +1178,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Re-render with steps[j].
await act(() =>
root.render(
@@ -1196,7 +1192,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(fallbackSnapshots[j]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
// Check that we can transition back again.
await act(() =>
root.render(
@@ -1209,7 +1205,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
expect(print(store)).toBe('');
@@ -1233,7 +1229,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]);
// Re-render with steps[j].
await act(() =>
root.render(
@@ -1247,7 +1243,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
expect(print(store, false, null, false)).toEqual(snapshots[j]);
// Check that we can transition back again.
await act(() =>
root.render(
@@ -1260,7 +1256,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
expect(print(store)).toBe('');
@@ -1291,7 +1287,7 @@ describe('StoreStressConcurrent', () => {
const suspenseID = store.getElementIDAtIndex(2);
// Force fallback.
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
await actAsync(async () => {
bridge.send('overrideSuspense', {
id: suspenseID,
@@ -1299,7 +1295,7 @@ describe('StoreStressConcurrent', () => {
forceFallback: true,
});
});
expect(print(store)).toEqual(fallbackSnapshots[j]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
// Stop forcing fallback.
await actAsync(async () => {
@@ -1309,7 +1305,7 @@ describe('StoreStressConcurrent', () => {
forceFallback: false,
});
});
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Trigger actual fallback.
await act(() =>
@@ -1323,7 +1319,7 @@ describe('StoreStressConcurrent', () => {
</Root>,
),
);
expect(print(store)).toEqual(fallbackSnapshots[j]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
// Force fallback while we're in fallback mode.
await act(() => {
@@ -1334,7 +1330,7 @@ describe('StoreStressConcurrent', () => {
});
});
// Keep seeing fallback content.
expect(print(store)).toEqual(fallbackSnapshots[j]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
// Switch to primary mode.
await act(() =>
@@ -1349,7 +1345,7 @@ describe('StoreStressConcurrent', () => {
),
);
// Fallback is still forced though.
expect(print(store)).toEqual(fallbackSnapshots[j]);
expect(print(store, false, null, false)).toEqual(fallbackSnapshots[j]);
// Stop forcing fallback. This reverts to primary content.
await actAsync(async () => {
@@ -1360,7 +1356,7 @@ describe('StoreStressConcurrent', () => {
});
});
// Now we see primary content.
expect(print(store)).toEqual(snapshots[i]);
expect(print(store, false, null, false)).toEqual(snapshots[i]);
// Clean up after every iteration.
await act(() => root.unmount());
@@ -1368,6 +1368,9 @@ describe('TreeListContext', () => {
<Child>
<Suspense>
<Grandchild>
[shell]
<Suspense name="Parent>?" rects={null}>
<Suspense name="Child>?" rects={null}>
`);
const outerSuspenseID = ((store.getElementIDAtIndex(1): any): number);
@@ -1407,6 +1410,9 @@ describe('TreeListContext', () => {
<Child>
<Suspense>
<Grandchild>
[shell]
<Suspense name="Parent>?" rects={null}>
<Suspense name="Child>?" rects={null}>
`);
});
});
@@ -2361,16 +2367,20 @@ describe('TreeListContext', () => {
jest.runAllTimers();
expect(state).toMatchInlineSnapshot(`
[root]
<Suspense>
`);
[root]
<Suspense>
[shell]
<Suspense name="Unknown" rects={null}>
`);
selectNextErrorOrWarning();
expect(state).toMatchInlineSnapshot(`
[root]
<Suspense>
`);
[root]
<Suspense>
[shell]
<Suspense name="Unknown" rects={null}>
`);
});
it('should properly handle errors/warnings from components that dont mount because of Suspense', async () => {
@@ -2392,9 +2402,11 @@ describe('TreeListContext', () => {
utils.act(() => TestRenderer.create(<Contexts />));
expect(state).toMatchInlineSnapshot(`
[root]
<Suspense>
`);
[root]
<Suspense>
[shell]
<Suspense name="Unknown" rects={null}>
`);
await Promise.resolve();
withErrorsOrWarningsIgnored(['test-only:'], () =>
@@ -2414,6 +2426,8 @@ describe('TreeListContext', () => {
<Suspense>
<Child>
<Child>
[shell]
<Suspense name="Unknown" rects={null}>
`);
});
@@ -2442,6 +2456,8 @@ describe('TreeListContext', () => {
<Suspense>
<Fallback>
<Child>
[shell]
<Suspense name="Unknown" rects={null}>
`);
await Promise.resolve();
@@ -2456,10 +2472,12 @@ describe('TreeListContext', () => {
);
expect(state).toMatchInlineSnapshot(`
[root]
<Suspense>
<Child>
`);
[root]
<Suspense>
<Child>
[shell]
<Suspense name="Unknown" rects={null}>
`);
});
});
File diff suppressed because it is too large Load Diff
@@ -426,6 +426,7 @@ export function attach(
pushOperation(ownerID);
pushOperation(displayNameStringID);
pushOperation(keyStringID);
pushOperation(getStringID(null)); // name prop
}
}
@@ -796,6 +797,7 @@ export function attach(
id: getID(owner),
key: element.key,
env: null,
stack: null,
type: getElementType(owner),
});
if (owner._currentElement) {
@@ -837,6 +839,8 @@ export function attach(
source: null,
stack: null,
// Only legacy context exists in legacy versions.
hasLegacyContext: true,
+4
View File
@@ -257,6 +257,7 @@ export type SerializedElement = {
id: number,
key: number | string | null,
env: null | string,
stack: null | ReactStackTrace,
type: ElementType,
};
@@ -308,6 +309,9 @@ export type InspectedElement = {
source: ReactFunctionLocation | null,
// The location of the JSX creation.
stack: ReactStackTrace | null,
type: ElementType,
// Meta information about the root this element belongs to.
@@ -284,7 +284,7 @@ export function parseStackTrace(
export function extractLocationFromOwnerStack(
error: Error,
): ReactFunctionLocation | null {
const stackTrace = parseStackTrace(error, 0);
const stackTrace = parseStackTrace(error, 1);
const stack = error.stack;
if (
!stack.includes('react_stack_bottom_frame') &&
+2
View File
@@ -257,6 +257,7 @@ export function convertInspectedElementBackendToFrontend(
owners,
env,
source,
stack,
context,
hooks,
plugins,
@@ -295,6 +296,7 @@ export function convertInspectedElementBackendToFrontend(
// Previous backend implementations (<= 6.1.5) have a different interface for Source.
// This gates the source features for only compatible backends: >= 6.1.6
source: Array.isArray(source) ? source : null,
stack: stack,
type,
owners:
owners === null
+4
View File
@@ -24,6 +24,10 @@ export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
export const TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS = 5;
export const TREE_OPERATION_REMOVE_ROOT = 6;
export const TREE_OPERATION_SET_SUBTREE_MODE = 7;
export const SUSPENSE_TREE_OPERATION_ADD = 8;
export const SUSPENSE_TREE_OPERATION_REMOVE = 9;
export const SUSPENSE_TREE_OPERATION_REORDER_CHILDREN = 10;
export const SUSPENSE_TREE_OPERATION_RESIZE = 11;
export const PROFILING_FLAG_BASIC_SUPPORT = 0b01;
export const PROFILING_FLAG_TIMELINE_SUPPORT = 0b10;
+296 -9
View File
@@ -20,6 +20,10 @@ import {
TREE_OPERATION_SET_SUBTREE_MODE,
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
SUSPENSE_TREE_OPERATION_ADD,
SUSPENSE_TREE_OPERATION_REMOVE,
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
SUSPENSE_TREE_OPERATION_RESIZE,
} from '../constants';
import {ElementTypeRoot} from '../frontend/types';
import {
@@ -44,6 +48,7 @@ import type {
Element,
ComponentFilter,
ElementType,
SuspenseNode,
} from 'react-devtools-shared/src/frontend/types';
import type {
FrontendBridge,
@@ -100,11 +105,12 @@ export default class Store extends EventEmitter<{
hookSettings: [$ReadOnly<DevToolsHookSettings>],
hostInstanceSelected: [Element['id']],
settingsUpdated: [$ReadOnly<DevToolsHookSettings>],
mutated: [[Array<number>, Map<number, number>]],
mutated: [[Array<Element['id']>, Map<Element['id'], Element['id']>]],
recordChangeDescriptions: [],
roots: [],
rootSupportsBasicProfiling: [],
rootSupportsTimelineProfiling: [],
suspenseTreeMutated: [],
supportsNativeStyleEditor: [],
supportsReloadAndProfile: [],
unsupportedBridgeProtocolDetected: [],
@@ -127,8 +133,10 @@ export default class Store extends EventEmitter<{
_componentFilters: Array<ComponentFilter>;
// Map of ID to number of recorded error and warning message IDs.
_errorsAndWarnings: Map<number, {errorCount: number, warningCount: number}> =
new Map();
_errorsAndWarnings: Map<
Element['id'],
{errorCount: number, warningCount: number},
> = new Map();
// At least one of the injected renderers contains (DEV only) owner metadata.
_hasOwnerMetadata: boolean = false;
@@ -136,7 +144,9 @@ export default class Store extends EventEmitter<{
// Map of ID to (mutable) Element.
// Elements are mutated to avoid excessive cloning during tree updates.
// The InspectedElement Suspense cache also relies on this mutability for its WeakMap usage.
_idToElement: Map<number, Element> = new Map();
_idToElement: Map<Element['id'], Element> = new Map();
_idToSuspense: Map<SuspenseNode['id'], SuspenseNode> = new Map();
// Should the React Native style editor panel be shown?
_isNativeStyleEditorSupported: boolean = false;
@@ -149,7 +159,7 @@ export default class Store extends EventEmitter<{
// Map of element (id) to the set of elements (ids) it owns.
// This map enables getOwnersListForElement() to avoid traversing the entire tree.
_ownersMap: Map<number, Set<number>> = new Map();
_ownersMap: Map<Element['id'], Set<Element['id']>> = new Map();
_profilerStore: ProfilerStore;
@@ -158,15 +168,16 @@ export default class Store extends EventEmitter<{
// Incremented each time the store is mutated.
// This enables a passive effect to detect a mutation between render and commit phase.
_revision: number = 0;
_revisionSuspense: number = 0;
// This Array must be treated as immutable!
// Passive effects will check it for changes between render and mount.
_roots: $ReadOnlyArray<number> = [];
_roots: $ReadOnlyArray<Element['id']> = [];
_rootIDToCapabilities: Map<number, Capabilities> = new Map();
_rootIDToCapabilities: Map<Element['id'], Capabilities> = new Map();
// Renderer ID is needed to support inspection fiber props, state, and hooks.
_rootIDToRendererID: Map<number, number> = new Map();
_rootIDToRendererID: Map<Element['id'], number> = new Map();
// These options may be initially set by a configuration option when constructing the Store.
_supportsInspectMatchingDOMElement: boolean = false;
@@ -439,6 +450,9 @@ export default class Store extends EventEmitter<{
get revision(): number {
return this._revision;
}
get revisionSuspense(): number {
return this._revisionSuspense;
}
get rootIDToRendererID(): Map<number, number> {
return this._rootIDToRendererID;
@@ -595,6 +609,16 @@ export default class Store extends EventEmitter<{
return element;
}
getSuspenseByID(id: SuspenseNode['id']): SuspenseNode | null {
const suspense = this._idToSuspense.get(id);
if (suspense === undefined) {
console.warn(`No suspense found with id "${id}"`);
return null;
}
return suspense;
}
// Returns a tuple of [id, index]
getElementsWithErrorsAndWarnings(): ErrorAndWarningTuples {
if (!this._shouldShowWarningsAndErrors) {
@@ -989,6 +1013,7 @@ export default class Store extends EventEmitter<{
let haveRootsChanged = false;
let haveErrorsOrWarningsChanged = false;
let hasSuspenseTreeChanged = false;
// The first two values are always rendererID and rootID
const rendererID = operations[0];
@@ -1092,6 +1117,7 @@ export default class Store extends EventEmitter<{
isCollapsed: false, // Never collapse roots; it would hide the entire tree.
isStrictModeNonCompliant,
key: null,
nameProp: null,
ownerID: 0,
parentID: 0,
type,
@@ -1115,6 +1141,10 @@ export default class Store extends EventEmitter<{
const key = stringTable[keyStringID];
i++;
const namePropStringID = operations[i];
const nameProp = stringTable[namePropStringID];
i++;
if (__DEBUG__) {
debug(
'Add',
@@ -1156,6 +1186,7 @@ export default class Store extends EventEmitter<{
isCollapsed: this._collapseNodesByDefault,
isStrictModeNonCompliant: parentElement.isStrictModeNonCompliant,
key,
nameProp,
ownerID,
parentID,
type,
@@ -1369,7 +1400,7 @@ export default class Store extends EventEmitter<{
// The profiler UI uses them lazily in order to generate the tree.
i += 3;
break;
case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS:
case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS: {
const id = operations[i + 1];
const errorCount = operations[i + 2];
const warningCount = operations[i + 3];
@@ -1383,6 +1414,255 @@ export default class Store extends EventEmitter<{
}
haveErrorsOrWarningsChanged = true;
break;
}
case SUSPENSE_TREE_OPERATION_ADD: {
const id = operations[i + 1];
const parentID = operations[i + 2];
const nameStringID = operations[i + 3];
const numRects = ((operations[i + 4]: any): number);
let name = stringTable[nameStringID];
if (this._idToSuspense.has(id)) {
this._throwAndEmitError(
Error(
`Cannot add suspense node "${id}" because a suspense node with that id is already in the Store.`,
),
);
}
const element = this._idToElement.get(id);
if (element === undefined) {
this._throwAndEmitError(
Error(
`Cannot add suspense node "${id}" because no matching element was found in the Store.`,
),
);
} else {
if (name === null) {
// The boundary isn't explicitly named.
// Pick a sensible default.
// TODO: Use key
const owner = this._idToElement.get(element.ownerID);
if (owner !== undefined) {
// TODO: This is clowny
name = `${owner.displayName || 'Unknown'}>?`;
}
}
}
i += 5;
let rects: SuspenseNode['rects'];
if (numRects === -1) {
rects = null;
} else {
rects = [];
for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
const x = operations[i + 0];
const y = operations[i + 1];
const width = operations[i + 2];
const height = operations[i + 3];
rects.push({x, y, width, height});
i += 4;
}
}
if (__DEBUG__) {
debug('Suspense Add', `node ${id} as child of ${parentID}`);
}
if (parentID !== 0) {
const parentSuspense = this._idToSuspense.get(parentID);
if (parentSuspense === undefined) {
this._throwAndEmitError(
Error(
`Cannot add suspense child "${id}" to parent suspense "${parentID}" because parent suspense node was not found in the Store.`,
),
);
break;
}
parentSuspense.children.push(id);
}
if (name === null) {
name = 'Unknown';
}
this._idToSuspense.set(id, {
id,
parentID,
children: [],
name,
rects,
});
hasSuspenseTreeChanged = true;
break;
}
case SUSPENSE_TREE_OPERATION_REMOVE: {
const removeLength = operations[i + 1];
i += 2;
for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
const id = operations[i];
const suspense = this._idToSuspense.get(id);
if (suspense === undefined) {
this._throwAndEmitError(
Error(
`Cannot remove suspense node "${id}" because no matching node was found in the Store.`,
),
);
break;
}
i += 1;
const {children, parentID} = suspense;
if (children.length > 0) {
this._throwAndEmitError(
Error(`Suspense node "${id}" was removed before its children.`),
);
}
this._idToSuspense.delete(id);
let parentSuspense: ?SuspenseNode = null;
if (parentID === 0) {
if (__DEBUG__) {
debug('Suspense remove', `node ${id} root`);
}
} else {
if (__DEBUG__) {
debug('Suspense Remove', `node ${id} from parent ${parentID}`);
}
parentSuspense = this._idToSuspense.get(parentID);
if (parentSuspense === undefined) {
this._throwAndEmitError(
Error(
`Cannot remove suspense node "${id}" from parent "${parentID}" because no matching node was found in the Store.`,
),
);
break;
}
const index = parentSuspense.children.indexOf(id);
parentSuspense.children.splice(index, 1);
}
}
hasSuspenseTreeChanged = true;
break;
}
case SUSPENSE_TREE_OPERATION_REORDER_CHILDREN: {
const id = operations[i + 1];
const numChildren = operations[i + 2];
i += 3;
const suspense = this._idToSuspense.get(id);
if (suspense === undefined) {
this._throwAndEmitError(
Error(
`Cannot reorder children for suspense node "${id}" because no matching node was found in the Store.`,
),
);
break;
}
const children = suspense.children;
if (children.length !== numChildren) {
this._throwAndEmitError(
Error(
`Suspense children cannot be added or removed during a reorder operation.`,
),
);
}
for (let j = 0; j < numChildren; j++) {
const childID = operations[i + j];
children[j] = childID;
if (__DEV__) {
// This check is more expensive so it's gated by __DEV__.
const childSuspense = this._idToSuspense.get(childID);
if (childSuspense == null || childSuspense.parentID !== id) {
console.error(
`Suspense children cannot be added or removed during a reorder operation.`,
);
}
}
}
i += numChildren;
if (__DEBUG__) {
debug(
'Re-order',
`Suspense node ${id} children ${children.join(',')}`,
);
}
hasSuspenseTreeChanged = true;
break;
}
case SUSPENSE_TREE_OPERATION_RESIZE: {
const id = ((operations[i + 1]: any): number);
const numRects = ((operations[i + 2]: any): number);
i += 3;
const suspense = this._idToSuspense.get(id);
if (suspense === undefined) {
this._throwAndEmitError(
Error(
`Cannot set rects for suspense node "${id}" because no matching node was found in the Store.`,
),
);
break;
}
let nextRects: SuspenseNode['rects'];
if (numRects === -1) {
nextRects = null;
} else {
nextRects = [];
for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
const x = operations[i + 0];
const y = operations[i + 1];
const width = operations[i + 2];
const height = operations[i + 3];
nextRects.push({x, y, width, height});
i += 4;
}
}
suspense.rects = nextRects;
if (__DEBUG__) {
debug(
'Resize',
`Suspense node ${id} resize to ${
nextRects === null
? 'null'
: nextRects
.map(
rect =>
`(${rect.x},${rect.y},${rect.width},${rect.height})`,
)
.join(',')
}`,
);
}
hasSuspenseTreeChanged = true;
break;
}
default:
this._throwAndEmitError(
new UnsupportedBridgeOperationError(
@@ -1393,6 +1673,9 @@ export default class Store extends EventEmitter<{
}
this._revision++;
if (hasSuspenseTreeChanged) {
this._revisionSuspense++;
}
// Any time the tree changes (e.g. elements added, removed, or reordered) cached indices may be invalid.
this._cachedErrorAndWarningTuples = null;
@@ -1451,6 +1734,10 @@ export default class Store extends EventEmitter<{
}
}
if (hasSuspenseTreeChanged) {
this.emit('suspenseTreeMutated');
}
if (__DEBUG__) {
console.log(printStore(this, true));
console.groupEnd();
+69 -2
View File
@@ -10,7 +10,10 @@
import JSON5 from 'json5';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import type {Element} from 'react-devtools-shared/src/frontend/types';
import type {
Element,
SuspenseNode,
} from 'react-devtools-shared/src/frontend/types';
import type {StateContext} from './views/Components/TreeContext';
import type Store from './store';
@@ -28,6 +31,11 @@ export function printElement(
key = ` key="${element.key}"`;
}
let name = '';
if (element.nameProp !== null) {
name = ` name="${element.nameProp}"`;
}
let hocDisplayNames = null;
if (element.hocDisplayNames !== null) {
hocDisplayNames = [...element.hocDisplayNames];
@@ -43,7 +51,45 @@ export function printElement(
return `${' '.repeat(element.depth + 1)}${prefix} <${
element.displayName || 'null'
}${key}>${hocs}${suffix}`;
}${key}${name}>${hocs}${suffix}`;
}
function printSuspense(
suspense: SuspenseNode,
includeWeight: boolean = false,
): string {
let name = '';
if (suspense.name !== null) {
name = ` name="${suspense.name}"`;
}
let printedRects = '';
const rects = suspense.rects;
if (rects === null) {
printedRects = ' rects={null}';
} else {
printedRects = ` rects={[${rects.map(rect => `{x:${rect.x},y:${rect.y},width:${rect.width},height:${rect.height}}`).join(', ')}]}`;
}
return `<Suspense${name}${printedRects}>`;
}
function printSuspenseWithChildren(
store: Store,
suspense: SuspenseNode,
depth: number,
): Array<string> {
const lines = [' '.repeat(depth) + printSuspense(suspense)];
for (let i = 0; i < suspense.children.length; i++) {
const childID = suspense.children[i];
const child = store.getSuspenseByID(childID);
if (child === null) {
throw new Error(`Could not find Suspense node with ID "${childID}".`);
}
lines.push(...printSuspenseWithChildren(store, child, depth + 1));
}
return lines;
}
export function printOwnersList(
@@ -59,6 +105,7 @@ export function printStore(
store: Store,
includeWeight: boolean = false,
state: StateContext | null = null,
includeSuspense: boolean = true,
): string {
const snapshotLines = [];
@@ -129,6 +176,26 @@ export function printStore(
}
rootWeight += weight;
if (includeSuspense) {
const shell = store.getSuspenseByID(rootID);
// Roots from legacy renderers don't have a separate Suspense tree
if (shell !== null) {
if (shell.children.length > 0) {
snapshotLines.push('[shell]');
for (let i = 0; i < shell.children.length; i++) {
const childID = shell.children[i];
const child = store.getSuspenseByID(childID);
if (child === null) {
throw new Error(
`Could not find Suspense node with ID "${childID}".`,
);
}
snapshotLines.push(...printSuspenseWithChildren(store, child, 1));
}
}
}
}
});
// Make sure the pretty-printed test align with the Store's reported number of total rows.
@@ -119,6 +119,7 @@ export default function Element({data, index, style}: Props): React.Node {
hocDisplayNames,
isStrictModeNonCompliant,
key,
nameProp,
compiledWithForget,
} = element;
const {
@@ -179,7 +180,24 @@ export default function Element({data, index, style}: Props): React.Node {
className={styles.KeyValue}
title={key}
onDoubleClick={handleKeyDoubleClick}>
<pre>{key}</pre>
<pre>
<IndexableDisplayName displayName={key} id={id} />
</pre>
</span>
"
</Fragment>
)}
{nameProp && (
<Fragment>
&nbsp;<span className={styles.KeyName}>name</span>="
<span
className={styles.KeyValue}
title={nameProp}
onDoubleClick={handleKeyDoubleClick}>
<pre>
<IndexableDisplayName displayName={nameProp} id={id} />
</pre>
</span>
"
</Fragment>
@@ -51,12 +51,19 @@ export default function InspectedElementWrapper(_: Props): React.Node {
const fetchFileWithCaching = useContext(FetchFileWithCachingContext);
const source =
inspectedElement == null
? null
: inspectedElement.source != null
? inspectedElement.source
: inspectedElement.stack != null && inspectedElement.stack.length > 0
? inspectedElement.stack[0]
: null;
const symbolicatedSourcePromise: null | Promise<ReactFunctionLocation | null> =
React.useMemo(() => {
if (inspectedElement == null) return null;
if (fetchFileWithCaching == null) return Promise.resolve(null);
const {source} = inspectedElement;
if (source == null) return Promise.resolve(null);
const [, sourceURL, line, column] = source;
@@ -66,7 +73,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
line,
column,
);
}, [inspectedElement]);
}, [source]);
const element =
inspectedElementID !== null
@@ -223,13 +230,12 @@ export default function InspectedElementWrapper(_: Props): React.Node {
{!alwaysOpenInEditor &&
!!editorURL &&
inspectedElement != null &&
inspectedElement.source != null &&
source != null &&
symbolicatedSourcePromise != null && (
<React.Suspense fallback={<Skeleton height={16} width={24} />}>
<OpenInEditorButton
editorURL={editorURL}
source={inspectedElement.source}
source={source}
symbolicatedSourcePromise={symbolicatedSourcePromise}
/>
</React.Suspense>
@@ -276,7 +282,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
{!hideViewSourceAction && (
<InspectedElementViewSourceButton
source={inspectedElement ? inspectedElement.source : null}
source={source}
symbolicatedSourcePromise={symbolicatedSourcePromise}
/>
)}
@@ -81,7 +81,21 @@ function SuspendedByRow({
}: RowProps) {
const [isOpen, setIsOpen] = useState(false);
const ioInfo = asyncInfo.awaited;
const name = ioInfo.name;
let name = ioInfo.name;
if (name === '' || name === 'Promise') {
// If all we have is a generic name, we can try to infer a better name from
// the stack. We only do this if the stack has more than one frame since
// otherwise it's likely to just be the name of the component which isn't better.
const bestStack = ioInfo.stack || asyncInfo.stack;
if (bestStack !== null && bestStack.length > 1) {
// TODO: Ideally we'd get the name from the last ignore listed frame before the
// first visible frame since this is the same algorithm as the Flight server uses.
// Ideally, we'd also get the name from the source mapped entry instead of the
// original entry. However, that would require suspending the immediate display
// of these rows to first do source mapping before we can show the name.
name = bestStack[0][0];
}
}
const description = ioInfo.description;
const longName = description === '' ? name : name + ' (' + description + ')';
const shortDescription = getShortDescription(name, description);
@@ -104,11 +118,15 @@ function SuspendedByRow({
// Only show the awaited stack if the I/O started in a different owner
// than where it was awaited. If it's started by the same component it's
// probably easy enough to infer and less noise in the common case.
const canShowAwaitStack =
(asyncInfo.stack !== null && asyncInfo.stack.length > 0) ||
(asyncOwner !== null && asyncOwner.id !== inspectedElement.id);
const showAwaitStack =
!showIOStack ||
(ioOwner === null
? asyncOwner !== null
: asyncOwner === null || ioOwner.id !== asyncOwner.id);
canShowAwaitStack &&
(!showIOStack ||
(ioOwner === null
? asyncOwner !== null
: asyncOwner === null || ioOwner.id !== asyncOwner.id));
const value: any = ioInfo.value;
const metaName =
@@ -160,9 +178,12 @@ function SuspendedByRow({
}
/>
)}
{(showIOStack || !showAwaitStack) &&
ioOwner !== null &&
ioOwner.id !== inspectedElement.id ? (
{ioOwner !== null &&
ioOwner.id !== inspectedElement.id &&
(showIOStack ||
!showAwaitStack ||
asyncOwner === null ||
ioOwner.id !== asyncOwner.id) ? (
<OwnerView
key={ioOwner.id}
displayName={ioOwner.displayName || 'Anonymous'}
@@ -22,6 +22,7 @@ import InspectedElementSuspendedBy from './InspectedElementSuspendedBy';
import NativeStyleEditor from './NativeStyleEditor';
import {enableStyleXFeatures} from 'react-devtools-feature-flags';
import InspectedElementSourcePanel from './InspectedElementSourcePanel';
import StackTraceView from './StackTraceView';
import OwnerView from './OwnerView';
import styles from './InspectedElementView.css';
@@ -52,6 +53,7 @@ export default function InspectedElementView({
symbolicatedSourcePromise,
}: Props): React.Node {
const {
stack,
owners,
rendererPackageName,
rendererVersion,
@@ -68,8 +70,9 @@ export default function InspectedElementView({
? `${rendererPackageName}@${rendererVersion}`
: null;
const showOwnersList = owners !== null && owners.length > 0;
const showStack = stack != null && stack.length > 0;
const showRenderedBy =
showOwnersList || rendererLabel !== null || rootType !== null;
showStack || showOwnersList || rendererLabel !== null || rootType !== null;
return (
<Fragment>
@@ -168,20 +171,25 @@ export default function InspectedElementView({
data-testname="InspectedElementView-Owners">
<div className={styles.OwnersHeader}>rendered by</div>
{showStack ? <StackTraceView stack={stack} /> : null}
{showOwnersList &&
owners?.map(owner => (
<OwnerView
key={owner.id}
displayName={owner.displayName || 'Anonymous'}
hocDisplayNames={owner.hocDisplayNames}
environmentName={
inspectedElement.env === owner.env ? null : owner.env
}
compiledWithForget={owner.compiledWithForget}
id={owner.id}
isInStore={store.containsElement(owner.id)}
type={owner.type}
/>
<Fragment key={owner.id}>
<OwnerView
displayName={owner.displayName || 'Anonymous'}
hocDisplayNames={owner.hocDisplayNames}
environmentName={
inspectedElement.env === owner.env ? null : owner.env
}
compiledWithForget={owner.compiledWithForget}
id={owner.id}
isInStore={store.containsElement(owner.id)}
type={owner.type}
/>
{owner.stack != null && owner.stack.length > 0 ? (
<StackTraceView stack={owner.stack} />
) : null}
</Fragment>
))}
{rootType !== null && (
@@ -60,7 +60,8 @@ export default function OwnerView({
<span className={styles.OwnerContent}>
<span
className={`${styles.Owner} ${isInStore ? '' : styles.NotInStore}`}
title={displayName}>
title={displayName}
data-testname="OwnerView">
{'<' + displayName + '>'}
</span>
@@ -995,7 +995,14 @@ function recursivelySearchTree(
return;
}
const {children, displayName, hocDisplayNames, compiledWithForget} = element;
const {
children,
displayName,
hocDisplayNames,
compiledWithForget,
key,
nameProp,
} = element;
if (displayName != null && regExp.test(displayName) === true) {
searchResults.push(elementID);
} else if (
@@ -1006,6 +1013,10 @@ function recursivelySearchTree(
searchResults.push(elementID);
} else if (compiledWithForget && regExp.test('Forget')) {
searchResults.push(elementID);
} else if (typeof key === 'string' && regExp.test(key)) {
searchResults.push(elementID);
} else if (typeof nameProp === 'string' && regExp.test(nameProp)) {
searchResults.push(elementID);
}
children.forEach(childID =>
+56 -48
View File
@@ -33,6 +33,7 @@ import FetchFileWithCachingContext from './Components/FetchFileWithCachingContex
import {InspectedElementContextController} from './Components/InspectedElementContext';
import HookNamesModuleLoaderContext from 'react-devtools-shared/src/devtools/views/Components/HookNamesModuleLoaderContext';
import {ProfilerContextController} from './Profiler/ProfilerContext';
import {SuspenseTreeContextController} from './SuspenseTab/SuspenseTreeContext';
import {TimelineContextController} from 'react-devtools-timeline/src/TimelineContext';
import {ModalDialogContextController} from './ModalDialog';
import ReactLogo from './ReactLogo';
@@ -319,58 +320,65 @@ export default function DevTools({
<ProfilerContextController>
<TimelineContextController>
<InspectedElementContextController>
<ThemeProvider>
<div
className={styles.DevTools}
ref={devToolsRef}
data-react-devtools-portal-root={true}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={selectTab}
tabs={tabs}
type="navigation"
<SuspenseTreeContextController>
<ThemeProvider>
<div
className={styles.DevTools}
ref={devToolsRef}
data-react-devtools-portal-root={true}>
{showTabBar && (
<div className={styles.TabBar}>
<ReactLogo />
<span
className={styles.DevToolsVersion}>
{process.env.DEVTOOLS_VERSION}
</span>
<div className={styles.Spacer} />
<TabBar
currentTab={tab}
id="DevTools"
selectTab={selectTab}
tabs={tabs}
type="navigation"
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}>
<Components
portalContainer={
componentsPortalContainer
}
/>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}>
<Profiler
portalContainer={
profilerPortalContainer
}
/>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'suspense'}>
<SuspenseTab
portalContainer={
suspensePortalContainer
}
/>
</div>
)}
<div
className={styles.TabContent}
hidden={tab !== 'components'}>
<Components
portalContainer={
componentsPortalContainer
}
/>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'profiler'}>
<Profiler
portalContainer={profilerPortalContainer}
{editorPortalContainer ? (
<EditorPane
selectedSource={currentSelectedSource}
portalContainer={editorPortalContainer}
/>
</div>
<div
className={styles.TabContent}
hidden={tab !== 'suspense'}>
<SuspenseTab
portalContainer={suspensePortalContainer}
/>
</div>
</div>
{editorPortalContainer ? (
<EditorPane
selectedSource={currentSelectedSource}
portalContainer={editorPortalContainer}
/>
) : null}
</ThemeProvider>
) : null}
</ThemeProvider>
</SuspenseTreeContextController>
</InspectedElementContextController>
</TimelineContextController>
</ProfilerContextController>
@@ -16,6 +16,10 @@ import {
TREE_OPERATION_SET_SUBTREE_MODE,
TREE_OPERATION_UPDATE_TREE_BASE_DURATION,
TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS,
SUSPENSE_TREE_OPERATION_ADD,
SUSPENSE_TREE_OPERATION_REMOVE,
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
SUSPENSE_TREE_OPERATION_RESIZE,
} from 'react-devtools-shared/src/constants';
import {
parseElementDisplayNameFromBackend,
@@ -236,6 +240,9 @@ function updateTree(
const key = stringTable[keyStringID];
i++;
// skip name prop
i++;
if (__DEBUG__) {
debug(
'Add',
@@ -366,6 +373,84 @@ function updateTree(
break;
}
case SUSPENSE_TREE_OPERATION_ADD: {
const fiberID = operations[i + 1];
const parentID = operations[i + 2];
const nameStringID = operations[i + 3];
const numRects = operations[i + 4];
const name = stringTable[nameStringID];
if (__DEBUG__) {
let rects: string;
if (numRects === -1) {
rects = 'null';
} else {
rects =
'[' +
operations.slice(i + 5, i + 5 + numRects * 4).join(',') +
']';
}
debug(
'Add suspense',
`node ${fiberID} (name=${JSON.stringify(name)}, rects={${rects}}) under ${parentID}`,
);
}
i += 5 + (numRects === -1 ? 0 : numRects * 4);
break;
}
case SUSPENSE_TREE_OPERATION_REMOVE: {
const removeLength = ((operations[i + 1]: any): number);
i += 2 + removeLength;
break;
}
case SUSPENSE_TREE_OPERATION_REORDER_CHILDREN: {
const suspenseID = ((operations[i + 1]: any): number);
const numChildren = ((operations[i + 2]: any): number);
const children = ((operations.slice(
i + 3,
i + 3 + numChildren,
): any): Array<number>);
i = i + 3 + numChildren;
if (__DEBUG__) {
debug(
'Suspense re-order',
`suspense ${suspenseID} children ${children.join(',')}`,
);
}
break;
}
case SUSPENSE_TREE_OPERATION_RESIZE: {
const suspenseID = ((operations[i + 1]: any): number);
const numRects = ((operations[i + 2]: any): number);
if (__DEBUG__) {
if (numRects === -1) {
debug('Suspense resize', `suspense ${suspenseID} rects null`);
} else {
const rects = ((operations.slice(
i + 3,
i + 3 + numRects * 4,
): any): Array<number>);
debug(
'Suspense resize',
`suspense ${suspenseID} rects [${rects.join(',')}]`,
);
}
}
i += 3 + (numRects === -1 ? 0 : numRects * 4);
break;
}
default:
throw Error(`Unsupported Bridge operation "${operation}"`);
}
@@ -19,6 +19,7 @@ import InspectedElementErrorBoundary from '../Components/InspectedElementErrorBo
import InspectedElement from '../Components/InspectedElement';
import portaledContent from '../portaledContent';
import styles from './SuspenseTab.css';
import SuspenseTreeList from './SuspenseTreeList';
import Button from '../Button';
type Orientation = 'horizontal' | 'vertical';
@@ -43,10 +44,6 @@ type LayoutState = {
};
type LayoutDispatch = (action: LayoutAction) => void;
function SuspenseTreeList() {
return <div>tree list</div>;
}
function SuspenseTimeline() {
return <div className={styles.Timeline}>timeline</div>;
}
@@ -0,0 +1,111 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ReactContext} from 'shared/ReactTypes';
import * as React from 'react';
import {
createContext,
startTransition,
useContext,
useEffect,
useMemo,
useReducer,
} from 'react';
import {StoreContext} from '../context';
export type SuspenseTreeState = {};
type ACTION_HANDLE_SUSPENSE_TREE_MUTATION = {
type: 'HANDLE_SUSPENSE_TREE_MUTATION',
};
export type SuspenseTreeAction = ACTION_HANDLE_SUSPENSE_TREE_MUTATION;
export type SuspenseTreeDispatch = (action: SuspenseTreeAction) => void;
const SuspenseTreeStateContext: ReactContext<SuspenseTreeState> =
createContext<SuspenseTreeState>(((null: any): SuspenseTreeState));
SuspenseTreeStateContext.displayName = 'SuspenseTreeStateContext';
const SuspenseTreeDispatcherContext: ReactContext<SuspenseTreeDispatch> =
createContext<SuspenseTreeDispatch>(((null: any): SuspenseTreeDispatch));
SuspenseTreeDispatcherContext.displayName = 'SuspenseTreeDispatcherContext';
type Props = {
children: React$Node,
};
function SuspenseTreeContextController({children}: Props): React.Node {
const store = useContext(StoreContext);
const initialRevision = useMemo(() => store.revisionSuspense, [store]);
// This reducer is created inline because it needs access to the Store.
// The store is mutable, but the Store itself is global and lives for the lifetime of the DevTools,
// so it's okay for the reducer to have an empty dependencies array.
const reducer = useMemo(
() =>
(
state: SuspenseTreeState,
action: SuspenseTreeAction,
): SuspenseTreeState => {
const {type} = action;
switch (type) {
case 'HANDLE_SUSPENSE_TREE_MUTATION':
return {...state};
default:
throw new Error(`Unrecognized action "${type}"`);
}
},
[],
);
const [state, dispatch] = useReducer(reducer, {});
const transitionDispatch = useMemo(
() => (action: SuspenseTreeAction) =>
startTransition(() => {
dispatch(action);
}),
[dispatch],
);
useEffect(() => {
const handleSuspenseTreeMutated = () => {
transitionDispatch({
type: 'HANDLE_SUSPENSE_TREE_MUTATION',
});
};
// Since this is a passive effect, the tree may have been mutated before our initial subscription.
if (store.revisionSuspense !== initialRevision) {
// At the moment, we can treat this as a mutation.
// We don't know which Elements were newly added/removed, but that should be okay in this case.
// It would only impact the search state, which is unlikely to exist yet at this point.
transitionDispatch({
type: 'HANDLE_SUSPENSE_TREE_MUTATION',
});
}
store.addListener('suspenseTreeMutated', handleSuspenseTreeMutated);
return () =>
store.removeListener('suspenseTreeMutated', handleSuspenseTreeMutated);
}, [dispatch, initialRevision, store]);
return (
<SuspenseTreeStateContext.Provider value={state}>
<SuspenseTreeDispatcherContext.Provider value={transitionDispatch}>
{children}
</SuspenseTreeDispatcherContext.Provider>
</SuspenseTreeStateContext.Provider>
);
}
export {
SuspenseTreeDispatcherContext,
SuspenseTreeStateContext,
SuspenseTreeContextController,
};
@@ -0,0 +1,90 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {SuspenseNode} from '../../../frontend/types';
import type Store from '../../store';
import * as React from 'react';
import {useContext} from 'react';
import {StoreContext} from '../context';
import {SuspenseTreeStateContext} from './SuspenseTreeContext';
import {TreeDispatcherContext} from '../Components/TreeContext';
function getDocumentOrderSuspenseTreeList(store: Store): Array<SuspenseNode> {
const suspenseTreeList: SuspenseNode[] = [];
for (let i = 0; i < store.roots.length; i++) {
const root = store.getElementByID(store.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;
}
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);
}
}
}
}
}
return suspenseTreeList;
}
export default function SuspenseTreeList(_: {}): React$Node {
const store = useContext(StoreContext);
const treeDispatch = useContext(TreeDispatcherContext);
useContext(SuspenseTreeStateContext);
const suspenseTreeList = getDocumentOrderSuspenseTreeList(store);
return (
<div>
<p>Suspense Tree List</p>
<ul>
{suspenseTreeList.map(suspense => {
const {id, parentID, children, name} = suspense;
return (
<li key={id}>
<div>
<button
onClick={() => {
treeDispatch({
type: 'SELECT_ELEMENT_BY_ID',
payload: id,
});
}}>
inspect {name || 'N/A'} ({id})
</button>
</div>
<div>
<strong>Suspense ID:</strong> {id}
</div>
<div>
<strong>Parent ID:</strong> {parentID}
</div>
<div>
<strong>Children:</strong>{' '}
{children.length === 0 ? '∅' : children.join(', ')}
</div>
</li>
);
})}
</ul>
</div>
);
}
+20
View File
@@ -157,6 +157,7 @@ export type Element = {
type: ElementType,
displayName: string | null,
key: number | string | null,
nameProp: null | string,
hocDisplayNames: null | Array<string>,
@@ -184,6 +185,21 @@ export type Element = {
compiledWithForget: boolean,
};
export type Rect = {
x: number,
y: number,
width: number,
height: number,
};
export type SuspenseNode = {
id: Element['id'],
parentID: SuspenseNode['id'] | 0,
children: Array<SuspenseNode['id']>,
name: string | null,
rects: null | Array<Rect>,
};
// Serialized version of ReactIOInfo
export type SerializedIOInfo = {
name: string,
@@ -209,6 +225,7 @@ export type SerializedElement = {
id: number,
key: number | string | null,
env: null | string,
stack: null | ReactStackTrace,
hocDisplayNames: Array<string> | null,
compiledWithForget: boolean,
type: ElementType,
@@ -272,6 +289,9 @@ export type InspectedElement = {
// Location of component in source code.
source: ReactFunctionLocation | null,
// The location of the JSX creation.
stack: ReactStackTrace | null,
type: ElementType,
// Meta information about the root this element belongs to.
+95 -7
View File
@@ -16,6 +16,8 @@ import {
setInObject,
} from 'react-devtools-shared/src/utils';
import {REACT_LEGACY_ELEMENT_TYPE} from 'shared/ReactSymbols';
import type {
DehydratedData,
InspectedElementPath,
@@ -188,18 +190,103 @@ export function dehydrate(
type,
};
// React Elements aren't very inspector-friendly,
// and often contain private fields or circular references.
case 'react_element':
cleaned.push(path);
return {
inspectable: false,
case 'react_element': {
isPathAllowedCheck = isPathAllowed(path);
if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
cleaned.push(path);
return {
inspectable: true,
preview_short: formatDataForPreview(data, false),
preview_long: formatDataForPreview(data, true),
name: getDisplayNameForReactElement(data) || 'Unknown',
type,
};
}
const unserializableValue: Unserializable = {
unserializable: true,
type,
readonly: true,
preview_short: formatDataForPreview(data, false),
preview_long: formatDataForPreview(data, true),
name: getDisplayNameForReactElement(data) || 'Unknown',
type,
};
// TODO: We can't expose type because that name is already taken on Unserializable.
unserializableValue.key = dehydrate(
data.key,
cleaned,
unserializable,
path.concat(['key']),
isPathAllowed,
isPathAllowedCheck ? 1 : level + 1,
);
if (data.$$typeof === REACT_LEGACY_ELEMENT_TYPE) {
unserializableValue.ref = dehydrate(
data.ref,
cleaned,
unserializable,
path.concat(['ref']),
isPathAllowed,
isPathAllowedCheck ? 1 : level + 1,
);
}
unserializableValue.props = dehydrate(
data.props,
cleaned,
unserializable,
path.concat(['props']),
isPathAllowed,
isPathAllowedCheck ? 1 : level + 1,
);
unserializable.push(path);
return unserializableValue;
}
case 'react_lazy': {
isPathAllowedCheck = isPathAllowed(path);
const payload = data._payload;
if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
cleaned.push(path);
const inspectable =
payload !== null &&
typeof payload === 'object' &&
(payload._status === 1 ||
payload._status === 2 ||
payload.status === 'fulfilled' ||
payload.status === 'rejected');
return {
inspectable,
preview_short: formatDataForPreview(data, false),
preview_long: formatDataForPreview(data, true),
name: 'lazy()',
type,
};
}
const unserializableValue: Unserializable = {
unserializable: true,
type: type,
preview_short: formatDataForPreview(data, false),
preview_long: formatDataForPreview(data, true),
name: 'lazy()',
};
// Ideally we should alias these properties to something more readable but
// unfortunately because of how the hydration algorithm uses a single concept of
// "path" we can't alias the path.
unserializableValue._payload = dehydrate(
payload,
cleaned,
unserializable,
path.concat(['_payload']),
isPathAllowed,
isPathAllowedCheck ? 1 : level + 1,
);
unserializable.push(path);
return unserializableValue;
}
// ArrayBuffers error if you try to inspect them.
case 'array_buffer':
case 'data_view':
@@ -309,6 +396,7 @@ export function dehydrate(
isPathAllowedCheck = isPathAllowed(path);
if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) {
cleaned.push(path);
return {
inspectable:
data.status === 'fulfilled' || data.status === 'rejected',
+158 -6
View File
@@ -40,6 +40,10 @@ import {
SESSION_STORAGE_RELOAD_AND_PROFILE_KEY,
SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY,
SESSION_STORAGE_RECORD_TIMELINE_KEY,
SUSPENSE_TREE_OPERATION_ADD,
SUSPENSE_TREE_OPERATION_REMOVE,
SUSPENSE_TREE_OPERATION_REORDER_CHILDREN,
SUSPENSE_TREE_OPERATION_RESIZE,
} from './constants';
import {
ComponentFilterElementType,
@@ -268,6 +272,7 @@ export function printOperationsArray(operations: Array<number>) {
i++;
i++; // key
i++; // name
logs.push(
`Add node ${id} (${displayName || 'null'}) as child of ${parentID}`,
@@ -318,7 +323,7 @@ export function printOperationsArray(operations: Array<number>) {
// The profiler UI uses them lazily in order to generate the tree.
i += 3;
break;
case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS:
case TREE_OPERATION_UPDATE_ERRORS_OR_WARNINGS: {
const id = operations[i + 1];
const numErrors = operations[i + 2];
const numWarnings = operations[i + 3];
@@ -329,6 +334,95 @@ export function printOperationsArray(operations: Array<number>) {
`Node ${id} has ${numErrors} errors and ${numWarnings} warnings`,
);
break;
}
case SUSPENSE_TREE_OPERATION_ADD: {
const fiberID = operations[i + 1];
const parentID = operations[i + 2];
const nameStringID = operations[i + 3];
const name = stringTable[nameStringID];
const numRects = operations[i + 4];
i += 5;
let rects: string;
if (numRects === -1) {
rects = 'null';
} else {
rects = '[';
for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
const offset = i + rectIndex * 4;
const x = operations[offset + 0];
const y = operations[offset + 1];
const width = operations[offset + 2];
const height = operations[offset + 3];
if (rectIndex > 0) {
rects += ', ';
}
rects += `(${x}, ${y}, ${width}, ${height})`;
i += 4;
}
rects += ']';
}
logs.push(
`Add suspense node ${fiberID} (${String(name)},rects={${rects}}) under ${parentID}`,
);
break;
}
case SUSPENSE_TREE_OPERATION_REMOVE: {
const removeLength = ((operations[i + 1]: any): number);
i += 2;
for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
const id = ((operations[i]: any): number);
i += 1;
logs.push(`Remove suspense node ${id}`);
}
break;
}
case SUSPENSE_TREE_OPERATION_REORDER_CHILDREN: {
const id = ((operations[i + 1]: any): number);
const numChildren = ((operations[i + 2]: any): number);
i += 3;
const children = operations.slice(i, i + numChildren);
i += numChildren;
logs.push(
`Re-order suspense node ${id} children ${children.join(',')}`,
);
break;
}
case SUSPENSE_TREE_OPERATION_RESIZE: {
const id = ((operations[i + 1]: any): number);
const numRects = ((operations[i + 2]: any): number);
i += 3;
if (numRects === -1) {
logs.push(`Resize suspense node ${id} to null`);
} else {
let line = `Resize suspense node ${id} to [`;
for (let rectIndex = 0; rectIndex < numRects; rectIndex++) {
const x = operations[i + 0];
const y = operations[i + 1];
const width = operations[i + 2];
const height = operations[i + 3];
if (rectIndex > 0) {
line += ', ';
}
line += `(${x}, ${y}, ${width}, ${height})`;
i += 4;
}
logs.push(line + ']');
}
break;
}
default:
throw Error(`Unsupported Bridge operation "${operation}"`);
}
@@ -591,6 +685,7 @@ export type DataType =
| 'thenable'
| 'object'
| 'react_element'
| 'react_lazy'
| 'regexp'
| 'string'
| 'symbol'
@@ -644,11 +739,12 @@ export function getDataType(data: Object): DataType {
return 'number';
}
case 'object':
if (
data.$$typeof === REACT_ELEMENT_TYPE ||
data.$$typeof === REACT_LEGACY_ELEMENT_TYPE
) {
return 'react_element';
switch (data.$$typeof) {
case REACT_ELEMENT_TYPE:
case REACT_LEGACY_ELEMENT_TYPE:
return 'react_element';
case REACT_LAZY_TYPE:
return 'react_lazy';
}
if (isArray(data)) {
return 'array';
@@ -864,6 +960,62 @@ export function formatDataForPreview(
return `<${truncateForDisplay(
getDisplayNameForReactElement(data) || 'Unknown',
)} />`;
case 'react_lazy':
// To avoid actually initialize a lazy to cause a side-effect we make some assumptions
// about the structure of the payload even though that's not really part of the contract.
// In practice, this is really just coming from React.lazy helper or Flight.
const payload = data._payload;
if (payload !== null && typeof payload === 'object') {
if (payload._status === 0) {
// React.lazy constructor pending
return `pending lazy()`;
}
if (payload._status === 1 && payload._result != null) {
// React.lazy constructor fulfilled
if (showFormattedValue) {
const formatted = formatDataForPreview(
payload._result.default,
false,
);
return `fulfilled lazy() {${truncateForDisplay(formatted)}}`;
} else {
return `fulfilled lazy() {…}`;
}
}
if (payload._status === 2) {
// React.lazy constructor rejected
if (showFormattedValue) {
const formatted = formatDataForPreview(payload._result, false);
return `rejected lazy() {${truncateForDisplay(formatted)}}`;
} else {
return `rejected lazy() {…}`;
}
}
if (payload.status === 'pending' || payload.status === 'blocked') {
// React Flight pending
return `pending lazy()`;
}
if (payload.status === 'fulfilled') {
// React Flight fulfilled
if (showFormattedValue) {
const formatted = formatDataForPreview(payload.value, false);
return `fulfilled lazy() {${truncateForDisplay(formatted)}}`;
} else {
return `fulfilled lazy() {…}`;
}
}
if (payload.status === 'rejected') {
// React Flight rejected
if (showFormattedValue) {
const formatted = formatDataForPreview(payload.reason, false);
return `rejected lazy() {${truncateForDisplay(formatted)}}`;
} else {
return `rejected lazy() {…}`;
}
}
}
// Some form of uninitialized
return 'lazy()';
case 'array_buffer':
return `ArrayBuffer(${data.byteLength})`;
case 'data_view':
+149 -1
View File
@@ -12,6 +12,7 @@ import {
Fragment,
Suspense,
unstable_SuspenseList as SuspenseList,
useReducer,
useState,
} from 'react';
@@ -26,10 +27,156 @@ function SuspenseTree(): React.Node {
<NestedSuspenseTest />
<SuspenseListTest />
<EmptySuspense />
<SuspenseTreeOperations />
</Fragment>
);
}
function IgnoreMePassthrough({children}: {children: React$Node}) {
return <span>{children}</span>;
}
const suspenseTreeOperationsChildren = {
a: (
<Suspense key="a" name="a">
<p>A</p>
</Suspense>
),
b: (
<div key="b">
<Suspense name="b">B</Suspense>
</div>
),
c: (
<p key="c">
<Suspense key="c" name="c">
C
</Suspense>
</p>
),
d: (
<Suspense key="d" name="d">
<div>D</div>
</Suspense>
),
e: (
<Suspense key="e" name="e">
<IgnoreMePassthrough key="e1">
<Suspense name="e-child-one">
<p>e1</p>
</Suspense>
</IgnoreMePassthrough>
<IgnoreMePassthrough key="e2">
<Suspense name="e-child-two">
<div>e2</div>
</Suspense>
</IgnoreMePassthrough>
</Suspense>
),
eReordered: (
<Suspense key="e" name="e">
<IgnoreMePassthrough key="e2">
<Suspense name="e-child-two">
<div>e2</div>
</Suspense>
</IgnoreMePassthrough>
<IgnoreMePassthrough key="e1">
<Suspense name="e-child-one">
<p>e1</p>
</Suspense>
</IgnoreMePassthrough>
</Suspense>
),
};
function SuspenseTreeOperations() {
const initialChildren: any[] = [
suspenseTreeOperationsChildren.a,
suspenseTreeOperationsChildren.b,
suspenseTreeOperationsChildren.c,
suspenseTreeOperationsChildren.d,
suspenseTreeOperationsChildren.e,
];
const [children, dispatch] = useReducer(
(
pendingState: any[],
action: 'toggle-mount' | 'reorder' | 'reorder-within-filtered',
): React$Node[] => {
switch (action) {
case 'toggle-mount':
if (pendingState.length === 5) {
return [
suspenseTreeOperationsChildren.a,
suspenseTreeOperationsChildren.b,
suspenseTreeOperationsChildren.c,
suspenseTreeOperationsChildren.d,
];
} else {
return [
suspenseTreeOperationsChildren.a,
suspenseTreeOperationsChildren.b,
suspenseTreeOperationsChildren.c,
suspenseTreeOperationsChildren.d,
suspenseTreeOperationsChildren.e,
];
}
case 'reorder':
if (pendingState[1] === suspenseTreeOperationsChildren.b) {
return [
suspenseTreeOperationsChildren.a,
suspenseTreeOperationsChildren.c,
suspenseTreeOperationsChildren.b,
suspenseTreeOperationsChildren.d,
suspenseTreeOperationsChildren.e,
];
} else {
return [
suspenseTreeOperationsChildren.a,
suspenseTreeOperationsChildren.b,
suspenseTreeOperationsChildren.c,
suspenseTreeOperationsChildren.d,
suspenseTreeOperationsChildren.e,
];
}
case 'reorder-within-filtered':
if (pendingState[4] === suspenseTreeOperationsChildren.e) {
return [
suspenseTreeOperationsChildren.a,
suspenseTreeOperationsChildren.b,
suspenseTreeOperationsChildren.c,
suspenseTreeOperationsChildren.d,
suspenseTreeOperationsChildren.eReordered,
];
} else {
return [
suspenseTreeOperationsChildren.a,
suspenseTreeOperationsChildren.b,
suspenseTreeOperationsChildren.c,
suspenseTreeOperationsChildren.d,
suspenseTreeOperationsChildren.e,
];
}
default:
return pendingState;
}
},
initialChildren,
);
return (
<>
<button onClick={() => dispatch('toggle-mount')}>Toggle Mount</button>
<button onClick={() => dispatch('reorder')}>Reorder</button>
<button onClick={() => dispatch('reorder-within-filtered')}>
Reorder Within Filtered
</button>
<Suspense name="operations-parent">
<section>{children}</section>
</Suspense>
</>
);
}
function EmptySuspense() {
return <Suspense />;
}
@@ -144,7 +291,8 @@ function LoadLater() {
<Suspense
fallback={
<Fallback1 onClick={() => setLoadChild(true)}>Click to load</Fallback1>
}>
}
name="LoadLater">
{loadChild ? (
<Primary1 onClick={() => setLoadChild(false)}>
Loaded! Click to suspend again.
+51 -3
View File
@@ -94,9 +94,7 @@ describe('ReactDOMFizzServer', () => {
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
ReactDOMFizzServer = require('react-dom/server');
if (__EXPERIMENTAL__) {
ReactDOMFizzStatic = require('react-dom/static');
}
ReactDOMFizzStatic = require('react-dom/static');
Stream = require('stream');
Suspense = React.Suspense;
use = React.use;
@@ -10784,4 +10782,54 @@ Unfortunately that previous paragraph wasn't quite long enough so I'll continue
// Instead we assert that we never emitted the fallback of the Suspense boundary around the body.
expect(streamedContent).not.toContain(randomTag);
});
it('should be able to Suspend after aborting in the same component without hanging the render', async () => {
const controller = new AbortController();
const promise1 = new Promise(() => {});
function AbortAndSuspend() {
controller.abort('boom');
return React.use(promise1);
}
function App() {
return (
<html>
<body>
<Suspense fallback="loading...">
{/*
The particular code path that was problematic required the Suspend to happen in renderNode
rather than retryRenderTask so we render the aborting function inside a host component
intentionally here
*/}
<div>
<AbortAndSuspend />
</div>
</Suspense>
</body>
</html>
);
}
const errors = [];
await act(async () => {
const result = await ReactDOMFizzStatic.prerenderToNodeStream(<App />, {
signal: controller.signal,
onError(e) {
errors.push(e);
},
});
result.prelude.pipe(writable);
});
expect(errors).toEqual(['boom']);
expect(getVisibleChildren(document)).toEqual(
<html>
<head />
<body>loading...</body>
</html>,
);
});
});
-18
View File
@@ -24,7 +24,6 @@ import type {ActivityInstance, SuspenseInstance} from './ReactFiberConfig';
import type {
LegacyHiddenProps,
OffscreenProps,
OffscreenInstance,
} from './ReactFiberOffscreenComponent';
import type {ViewTransitionState} from './ReactFiberViewTransitionComponent';
import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent';
@@ -76,7 +75,6 @@ import {
ViewTransitionComponent,
ActivityComponent,
} from './ReactWorkTags';
import {OffscreenVisible} from './ReactFiberOffscreenComponent';
import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber';
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
import {
@@ -831,13 +829,6 @@ export function createFiberFromOffscreen(
): Fiber {
const fiber = createFiber(OffscreenComponent, pendingProps, key, mode);
fiber.lanes = lanes;
const primaryChildInstance: OffscreenInstance = {
_visibility: OffscreenVisible,
_pendingMarkers: null,
_retryCache: null,
_transitions: null,
};
fiber.stateNode = primaryChildInstance;
return fiber;
}
export function createFiberFromActivity(
@@ -885,15 +876,6 @@ export function createFiberFromLegacyHidden(
const fiber = createFiber(LegacyHiddenComponent, pendingProps, key, mode);
fiber.elementType = REACT_LEGACY_HIDDEN_TYPE;
fiber.lanes = lanes;
// Adding a stateNode for legacy hidden because it's currently using
// the offscreen implementation, which depends on a state node
const instance: OffscreenInstance = {
_visibility: OffscreenVisible,
_pendingMarkers: null,
_transitions: null,
_retryCache: null,
};
fiber.stateNode = instance;
return fiber;
}
+54 -18
View File
@@ -280,6 +280,7 @@ import {
createCapturedValueFromError,
createCapturedValueAtFiber,
} from './ReactCapturedValue';
import {OffscreenVisible} from './ReactFiberOffscreenComponent';
import {
createClassErrorUpdate,
initializeClassErrorUpdate,
@@ -620,6 +621,18 @@ function updateOffscreenComponent(
const prevState: OffscreenState | null =
current !== null ? current.memoizedState : null;
if (current === null && workInProgress.stateNode === null) {
// We previously reset the work-in-progress.
// We need to create a new Offscreen instance.
const primaryChildInstance: OffscreenInstance = {
_visibility: OffscreenVisible,
_pendingMarkers: null,
_retryCache: null,
_transitions: null,
};
workInProgress.stateNode = primaryChildInstance;
}
if (
nextProps.mode === 'hidden' ||
(enableLegacyHidden && nextProps.mode === 'unstable-defer-without-hiding')
@@ -788,6 +801,26 @@ function updateOffscreenComponent(
return workInProgress.child;
}
function bailoutOffscreenComponent(
current: Fiber | null,
workInProgress: Fiber,
): Fiber | null {
if (
(current === null || current.tag !== OffscreenComponent) &&
workInProgress.stateNode === null
) {
const primaryChildInstance: OffscreenInstance = {
_visibility: OffscreenVisible,
_pendingMarkers: null,
_retryCache: null,
_transitions: null,
};
workInProgress.stateNode = primaryChildInstance;
}
return workInProgress.sibling;
}
function deferHiddenOffscreenComponent(
current: Fiber | null,
workInProgress: Fiber,
@@ -1095,9 +1128,13 @@ function updateActivityComponent(
if (nextProps.mode === 'hidden') {
// SSR doesn't render hidden Activity so it shouldn't hydrate,
// even at offscreen lane. Defer to a client rendered offscreen lane.
mountActivityChildren(workInProgress, nextProps, renderLanes);
const primaryChildFragment = mountActivityChildren(
workInProgress,
nextProps,
renderLanes,
);
workInProgress.lanes = laneToLanes(OffscreenLane);
return null;
return bailoutOffscreenComponent(null, primaryChildFragment);
} else {
// We must push the suspense handler context *before* attempting to
// hydrate, to avoid a mismatch in case it errors.
@@ -2373,7 +2410,7 @@ function updateSuspenseComponent(
if (showFallback) {
pushFallbackTreeSuspenseHandler(workInProgress);
const fallbackFragment = mountSuspenseFallbackChildren(
mountSuspenseFallbackChildren(
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
@@ -2408,7 +2445,7 @@ function updateSuspenseComponent(
}
}
return fallbackFragment;
return bailoutOffscreenComponent(null, primaryChildFragment);
} else if (
enableCPUSuspense &&
typeof nextProps.unstable_expectedLoadTime === 'number'
@@ -2417,7 +2454,7 @@ function updateSuspenseComponent(
// unblock the surrounding content. Then immediately retry after the
// initial commit.
pushFallbackTreeSuspenseHandler(workInProgress);
const fallbackFragment = mountSuspenseFallbackChildren(
mountSuspenseFallbackChildren(
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
@@ -2444,7 +2481,7 @@ function updateSuspenseComponent(
// RetryLane even if it's the one currently rendering since we're leaving
// it behind on this node.
workInProgress.lanes = SomeRetryLane;
return fallbackFragment;
return bailoutOffscreenComponent(null, primaryChildFragment);
} else {
pushPrimaryTreeSuspenseHandler(workInProgress);
return mountSuspensePrimaryChildren(
@@ -2479,7 +2516,7 @@ function updateSuspenseComponent(
const nextFallbackChildren = nextProps.fallback;
const nextPrimaryChildren = nextProps.children;
const fallbackChildFragment = updateSuspenseFallbackChildren(
updateSuspenseFallbackChildren(
current,
workInProgress,
nextPrimaryChildren,
@@ -2532,7 +2569,7 @@ function updateSuspenseComponent(
renderLanes,
);
workInProgress.memoizedState = SUSPENDED_MARKER;
return fallbackChildFragment;
return bailoutOffscreenComponent(current.child, primaryChildFragment);
} else {
if (
prevState !== null &&
@@ -2788,7 +2825,7 @@ function updateSuspenseFallbackChildren(
primaryChildFragment.sibling = fallbackChildFragment;
workInProgress.child = primaryChildFragment;
return fallbackChildFragment;
return bailoutOffscreenComponent(null, primaryChildFragment);
}
function retrySuspenseComponentWithoutHydrating(
@@ -3094,14 +3131,13 @@ function updateDehydratedSuspenseComponent(
const nextPrimaryChildren = nextProps.children;
const nextFallbackChildren = nextProps.fallback;
const fallbackChildFragment =
mountSuspenseFallbackAfterRetryWithoutHydrating(
current,
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
renderLanes,
);
mountSuspenseFallbackAfterRetryWithoutHydrating(
current,
workInProgress,
nextPrimaryChildren,
nextFallbackChildren,
renderLanes,
);
const primaryChildFragment: Fiber = (workInProgress.child: any);
primaryChildFragment.memoizedState =
mountSuspenseOffscreenState(renderLanes);
@@ -3111,7 +3147,7 @@ function updateDehydratedSuspenseComponent(
renderLanes,
);
workInProgress.memoizedState = SUSPENDED_MARKER;
return fallbackChildFragment;
return bailoutOffscreenComponent(null, primaryChildFragment);
}
}
}
+59 -59
View File
@@ -261,74 +261,74 @@ function scheduleFibersWithFamiliesRecursively(
staleFamilies: Set<Family>,
): void {
if (__DEV__) {
const {alternate, child, sibling, tag, type} = fiber;
do {
const {alternate, child, sibling, tag, type} = fiber;
let candidateType = null;
switch (tag) {
case FunctionComponent:
case SimpleMemoComponent:
case ClassComponent:
candidateType = type;
break;
case ForwardRef:
candidateType = type.render;
break;
default:
break;
}
let candidateType = null;
switch (tag) {
case FunctionComponent:
case SimpleMemoComponent:
case ClassComponent:
candidateType = type;
break;
case ForwardRef:
candidateType = type.render;
break;
default:
break;
}
if (resolveFamily === null) {
throw new Error('Expected resolveFamily to be set during hot reload.');
}
if (resolveFamily === null) {
throw new Error('Expected resolveFamily to be set during hot reload.');
}
let needsRender = false;
let needsRemount = false;
if (candidateType !== null) {
const family = resolveFamily(candidateType);
if (family !== undefined) {
if (staleFamilies.has(family)) {
needsRemount = true;
} else if (updatedFamilies.has(family)) {
if (tag === ClassComponent) {
let needsRender = false;
let needsRemount = false;
if (candidateType !== null) {
const family = resolveFamily(candidateType);
if (family !== undefined) {
if (staleFamilies.has(family)) {
needsRemount = true;
} else {
needsRender = true;
} else if (updatedFamilies.has(family)) {
if (tag === ClassComponent) {
needsRemount = true;
} else {
needsRender = true;
}
}
}
}
}
if (failedBoundaries !== null) {
if (
failedBoundaries.has(fiber) ||
// $FlowFixMe[incompatible-use] found when upgrading Flow
(alternate !== null && failedBoundaries.has(alternate))
) {
needsRemount = true;
if (failedBoundaries !== null) {
if (
failedBoundaries.has(fiber) ||
// $FlowFixMe[incompatible-use] found when upgrading Flow
(alternate !== null && failedBoundaries.has(alternate))
) {
needsRemount = true;
}
}
}
if (needsRemount) {
fiber._debugNeedsRemount = true;
}
if (needsRemount || needsRender) {
const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, SyncLane);
if (needsRemount) {
fiber._debugNeedsRemount = true;
}
}
if (child !== null && !needsRemount) {
scheduleFibersWithFamiliesRecursively(
child,
updatedFamilies,
staleFamilies,
);
}
if (sibling !== null) {
scheduleFibersWithFamiliesRecursively(
sibling,
updatedFamilies,
staleFamilies,
);
}
if (needsRemount || needsRender) {
const root = enqueueConcurrentRenderForLane(fiber, SyncLane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, SyncLane);
}
}
if (child !== null && !needsRemount) {
scheduleFibersWithFamiliesRecursively(
child,
updatedFamilies,
staleFamilies,
);
}
if (sibling === null) {
break;
}
fiber = sibling;
} while (true);
}
}
+30
View File
@@ -12,6 +12,7 @@ import type {
PendingThenable,
FulfilledThenable,
RejectedThenable,
ReactIOInfo,
} from 'shared/ReactTypes';
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
@@ -22,6 +23,8 @@ import {getWorkInProgressRoot} from './ReactFiberWorkLoop';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
import noop from 'shared/noop';
opaque type ThenableStateDev = {
@@ -154,6 +157,33 @@ export function trackUsedThenable<T>(
}
}
if (__DEV__ && enableAsyncDebugInfo && thenable._debugInfo === undefined) {
// In DEV mode if the thenable that we observed had no debug info, then we add
// an inferred debug info so that we're able to track its potential I/O uniquely.
// We don't know the real start time since the I/O could have started much
// earlier and this could even be a cached Promise. Could be misleading.
const startTime = performance.now();
const displayName = thenable.displayName;
const ioInfo: ReactIOInfo = {
name: typeof displayName === 'string' ? displayName : 'Promise',
start: startTime,
end: startTime,
value: (thenable: any),
// We don't know the requesting owner nor stack.
};
// We can infer the await owner/stack lazily from where this promise ends up
// used. It can be used in more than one place so we can't assign it here.
thenable._debugInfo = [{awaited: ioInfo}];
// Track when we resolved the Promise as the approximate end time.
if (thenable.status !== 'fulfilled' && thenable.status !== 'rejected') {
const trackEndTime = () => {
// $FlowFixMe[cannot-write]
ioInfo.end = performance.now();
};
thenable.then(trackEndTime, trackEndTime);
}
}
// We use an expando to track the status and result of a thenable so that we
// can synchronously unwrap the value. Think of this as an extension of the
// Promise API, or a custom interface that is a superset of Thenable.
@@ -4141,4 +4141,28 @@ describe('ReactSuspenseWithNoopRenderer', () => {
</>,
);
});
it('can rerender after resolving a promise', async () => {
const promise = Promise.resolve(null);
const root = ReactNoop.createRoot();
await act(() => {
startTransition(() => {
root.render(<Suspense>{promise}</Suspense>);
});
});
assertLog([]);
expect(root).toMatchRenderedOutput(null);
await act(() => {
startTransition(() => {
root.render(
<Suspense>
<div />
</Suspense>,
);
});
});
});
});
+6 -2
View File
@@ -4155,7 +4155,9 @@ function renderNode(
getSuspendedThenable()
: thrownValue;
if (typeof x === 'object' && x !== null) {
if (request.status === ABORTING) {
// We are aborting so we can just bubble up to the task by falling through
} else if (typeof x === 'object' && x !== null) {
// $FlowFixMe[method-unbinding]
if (typeof x.then === 'function') {
const wakeable: Wakeable = (x: any);
@@ -4254,7 +4256,9 @@ function renderNode(
getSuspendedThenable()
: thrownValue;
if (typeof x === 'object' && x !== null) {
if (request.status === ABORTING) {
// We are aborting so we can just bubble up to the task by falling through
} else if (typeof x === 'object' && x !== null) {
// $FlowFixMe[method-unbinding]
if (typeof x.then === 'function') {
const wakeable: Wakeable = (x: any);
+86 -1
View File
@@ -7,7 +7,16 @@
* @flow
*/
import type {Wakeable, Thenable, ReactDebugInfo} from 'shared/ReactTypes';
import type {
Wakeable,
Thenable,
FulfilledThenable,
RejectedThenable,
ReactDebugInfo,
ReactIOInfo,
} from 'shared/ReactTypes';
import {enableAsyncDebugInfo} from 'shared/ReactFeatureFlags';
import {REACT_LAZY_TYPE} from 'shared/ReactSymbols';
@@ -19,21 +28,25 @@ const Rejected = 2;
type UninitializedPayload<T> = {
_status: -1,
_result: () => Thenable<{default: T, ...}>,
_ioInfo?: ReactIOInfo, // DEV-only
};
type PendingPayload = {
_status: 0,
_result: Wakeable,
_ioInfo?: ReactIOInfo, // DEV-only
};
type ResolvedPayload<T> = {
_status: 1,
_result: {default: T, ...},
_ioInfo?: ReactIOInfo, // DEV-only
};
type RejectedPayload = {
_status: 2,
_result: mixed,
_ioInfo?: ReactIOInfo, // DEV-only
};
type Payload<T> =
@@ -51,6 +64,14 @@ export type LazyComponent<T, P> = {
function lazyInitializer<T>(payload: Payload<T>): T {
if (payload._status === Uninitialized) {
if (__DEV__ && enableAsyncDebugInfo) {
const ioInfo = payload._ioInfo;
if (ioInfo != null) {
// Mark when we first kicked off the lazy request.
// $FlowFixMe[cannot-write]
ioInfo.start = ioInfo.end = performance.now();
}
}
const ctor = payload._result;
const thenable = ctor();
// Transition to the next state.
@@ -68,6 +89,21 @@ function lazyInitializer<T>(payload: Payload<T>): T {
const resolved: ResolvedPayload<T> = (payload: any);
resolved._status = Resolved;
resolved._result = moduleObject;
if (__DEV__) {
const ioInfo = payload._ioInfo;
if (ioInfo != null) {
// Mark the end time of when we resolved.
// $FlowFixMe[cannot-write]
ioInfo.end = performance.now();
}
// Make the thenable introspectable
if (thenable.status === undefined) {
const fulfilledThenable: FulfilledThenable<{default: T, ...}> =
(thenable: any);
fulfilledThenable.status = 'fulfilled';
fulfilledThenable.value = moduleObject;
}
}
}
},
error => {
@@ -79,9 +115,37 @@ function lazyInitializer<T>(payload: Payload<T>): T {
const rejected: RejectedPayload = (payload: any);
rejected._status = Rejected;
rejected._result = error;
if (__DEV__ && enableAsyncDebugInfo) {
const ioInfo = payload._ioInfo;
if (ioInfo != null) {
// Mark the end time of when we rejected.
// $FlowFixMe[cannot-write]
ioInfo.end = performance.now();
}
// Make the thenable introspectable
if (thenable.status === undefined) {
const rejectedThenable: RejectedThenable<{default: T, ...}> =
(thenable: any);
rejectedThenable.status = 'rejected';
rejectedThenable.reason = error;
}
}
}
},
);
if (__DEV__ && enableAsyncDebugInfo) {
const ioInfo = payload._ioInfo;
if (ioInfo != null) {
// Stash the thenable for introspection of the value later.
// $FlowFixMe[cannot-write]
ioInfo.value = thenable;
const displayName = thenable.displayName;
if (typeof displayName === 'string') {
// $FlowFixMe[cannot-write]
ioInfo.name = displayName;
}
}
}
if (payload._status === Uninitialized) {
// In case, we're still uninitialized, then we're waiting for the thenable
// to resolve. Set it as pending in the meantime.
@@ -140,5 +204,26 @@ export function lazy<T>(
_init: lazyInitializer,
};
if (__DEV__ && enableAsyncDebugInfo) {
// TODO: We should really track the owner here but currently ReactIOInfo
// can only contain ReactComponentInfo and not a Fiber. It's unusual to
// create a lazy inside an owner though since they should be in module scope.
const owner = null;
const ioInfo: ReactIOInfo = {
name: 'lazy',
start: -1,
end: -1,
value: null,
owner: owner,
debugStack: new Error('react-stack-top-frame'),
// eslint-disable-next-line react-internal/no-production-logging
debugTask: console.createTask ? console.createTask('lazy()') : null,
};
payload._ioInfo = ioInfo;
// Add debug info to the lazy, but this doesn't have an await stack yet.
// That will be inferred by later usage.
lazyType._debugInfo = [{awaited: ioInfo}];
}
return lazyType;
}
+13 -71
View File
@@ -156,30 +156,9 @@ function elementRefGetterWithDeprecationWarning() {
* will not work. Instead test $$typeof field against Symbol.for('react.transitional.element') to check
* if something is a React Element.
*
* @param {*} type
* @param {*} props
* @param {*} key
* @param {string|object} ref
* @param {*} owner
* @param {*} self A *temporary* helper to detect places where `this` is
* different from the `owner` when React.createElement is called, so that we
* can warn. We want to get rid of owner and replace string `ref`s with arrow
* functions, and as long as `this` and owner are the same, there will be no
* change in behavior.
* @param {*} source An annotation object (added by a transpiler or otherwise)
* indicating filename, line number, and/or other information.
* @internal
*/
function ReactElement(
type,
key,
self,
source,
owner,
props,
debugStack,
debugTask,
) {
function ReactElement(type, key, props, owner, debugStack, debugTask) {
// Ignore whatever was passed as the ref argument and treat `props.ref` as
// the source of truth. The only thing we use this for is `element.ref`,
// which will log a deprecation warning on access. In the next release, we
@@ -348,16 +327,7 @@ export function jsxProd(type, config, maybeKey) {
}
}
return ReactElement(
type,
key,
undefined,
undefined,
getOwner(),
props,
undefined,
undefined,
);
return ReactElement(type, key, props, getOwner(), undefined, undefined);
}
// While `jsxDEV` should never be called when running in production, we do
@@ -376,8 +346,6 @@ export function jsxProdSignatureRunningInDevWithDynamicChildren(
type,
config,
maybeKey,
source,
self,
) {
if (__DEV__) {
const isStaticChildren = false;
@@ -389,8 +357,6 @@ export function jsxProdSignatureRunningInDevWithDynamicChildren(
config,
maybeKey,
isStaticChildren,
source,
self,
__DEV__ &&
(trackActualOwner
? Error('react-stack-top-frame')
@@ -407,8 +373,6 @@ export function jsxProdSignatureRunningInDevWithStaticChildren(
type,
config,
maybeKey,
source,
self,
) {
if (__DEV__) {
const isStaticChildren = true;
@@ -420,8 +384,6 @@ export function jsxProdSignatureRunningInDevWithStaticChildren(
config,
maybeKey,
isStaticChildren,
source,
self,
__DEV__ &&
(trackActualOwner
? Error('react-stack-top-frame')
@@ -442,7 +404,7 @@ const didWarnAboutKeySpread = {};
* @param {object} props
* @param {string} key
*/
export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
export function jsxDEV(type, config, maybeKey, isStaticChildren) {
const trackActualOwner =
__DEV__ &&
ReactSharedInternals.recentlyCreatedOwnerStacks++ < ownerStackLimit;
@@ -451,8 +413,6 @@ export function jsxDEV(type, config, maybeKey, isStaticChildren, source, self) {
config,
maybeKey,
isStaticChildren,
source,
self,
__DEV__ &&
(trackActualOwner
? Error('react-stack-top-frame')
@@ -469,8 +429,6 @@ function jsxDEVImpl(
config,
maybeKey,
isStaticChildren,
source,
self,
debugStack,
debugTask,
) {
@@ -491,7 +449,7 @@ function jsxDEVImpl(
if (isStaticChildren) {
if (isArray(children)) {
for (let i = 0; i < children.length; i++) {
validateChildKeys(children[i], type);
validateChildKeys(children[i]);
}
if (Object.freeze) {
@@ -505,7 +463,7 @@ function jsxDEVImpl(
);
}
} else {
validateChildKeys(children, type);
validateChildKeys(children);
}
}
@@ -591,16 +549,7 @@ function jsxDEVImpl(
defineKeyPropWarningGetter(props, displayName);
}
return ReactElement(
type,
key,
self,
source,
getOwner(),
props,
debugStack,
debugTask,
);
return ReactElement(type, key, props, getOwner(), debugStack, debugTask);
}
}
@@ -620,7 +569,7 @@ export function createElement(type, config, children) {
// prod. (Rendering will throw with a helpful message and as soon as the
// type is fixed, the key warnings will appear.)
for (let i = 2; i < arguments.length; i++) {
validateChildKeys(arguments[i], type);
validateChildKeys(arguments[i]);
}
// Unlike the jsx() runtime, createElement() doesn't warn about key spread.
@@ -721,10 +670,8 @@ export function createElement(type, config, children) {
return ReactElement(
type,
key,
undefined,
undefined,
getOwner(),
props,
getOwner(),
__DEV__ &&
(trackActualOwner
? Error('react-stack-top-frame')
@@ -740,10 +687,8 @@ export function cloneAndReplaceKey(oldElement, newKey) {
const clonedElement = ReactElement(
oldElement.type,
newKey,
undefined,
undefined,
!__DEV__ ? undefined : oldElement._owner,
oldElement.props,
!__DEV__ ? undefined : oldElement._owner,
__DEV__ && oldElement._debugStack,
__DEV__ && oldElement._debugTask,
);
@@ -829,16 +774,14 @@ export function cloneElement(element, config, children) {
const clonedElement = ReactElement(
element.type,
key,
undefined,
undefined,
owner,
props,
owner,
__DEV__ && element._debugStack,
__DEV__ && element._debugTask,
);
for (let i = 2; i < arguments.length; i++) {
validateChildKeys(arguments[i], clonedElement.type);
validateChildKeys(arguments[i]);
}
return clonedElement;
@@ -853,10 +796,9 @@ export function cloneElement(element, config, children) {
* @param {ReactNode} node Statically passed child of any type.
* @param {*} parentType node's parent's type.
*/
function validateChildKeys(node, parentType) {
function validateChildKeys(node) {
if (__DEV__) {
// With owner stacks is, no warnings happens. All we do is
// mark elements as being in a valid static child position so they
// Mark elements as being in a valid static child position so they
// don't need keys.
if (isValidElement(node)) {
if (node._store) {
+2
View File
@@ -24,6 +24,8 @@ export function getIODescription(value: any): string {
return String(value.message);
} else if (typeof value.url === 'string') {
return value.url;
} else if (typeof value.href === 'string') {
return value.href;
} else if (typeof value.command === 'string') {
return value.command;
} else if (
+2
View File
@@ -108,6 +108,7 @@ interface ThenableImpl<T> {
onFulfill: (value: T) => mixed,
onReject: (error: mixed) => mixed,
): void | Wakeable;
displayName?: string;
}
interface UntrackedThenable<T> extends ThenableImpl<T> {
status?: void;
@@ -298,6 +299,7 @@ export type ViewTransitionProps = {
export type ActivityProps = {
mode?: 'hidden' | 'visible' | null | void,
children?: ReactNodeList,
name?: string,
};
export type SuspenseProps = {
@@ -13,7 +13,6 @@ import typeof * as ExportsType from './ReactFeatureFlags.test-renderer';
export const alwaysThrottleRetries = false;
export const disableClientCache = true;
export const disableCommentsAsDOMContainers = true;
export const disableDefaultPropsExceptForClasses = true;
export const disableInputAttributeSyncing = false;
export const disableLegacyContext = false;
export const disableLegacyContextForFunctionComponents = false;