mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge 38e718ed68 into sapling-pr-archive-poteto
This commit is contained in:
+9
-9
@@ -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
@@ -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,
|
||||
},
|
||||
],
|
||||
|
||||
+6
@@ -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
@@ -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={[]}>
|
||||
`);
|
||||
});
|
||||
|
||||
|
||||
+47
-51
@@ -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}>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+689
-163
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,
|
||||
|
||||
|
||||
@@ -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') &&
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
<span className={styles.KeyName}>name</span>="
|
||||
<span
|
||||
className={styles.KeyValue}
|
||||
title={nameProp}
|
||||
onDoubleClick={handleKeyDoubleClick}>
|
||||
<pre>
|
||||
<IndexableDisplayName displayName={nameProp} id={id} />
|
||||
</pre>
|
||||
</span>
|
||||
"
|
||||
</Fragment>
|
||||
|
||||
+13
-7
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
Vendored
+29
-8
@@ -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'}
|
||||
|
||||
+21
-13
@@ -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>
|
||||
|
||||
|
||||
+12
-1
@@ -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
@@ -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>
|
||||
|
||||
+85
@@ -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}"`);
|
||||
}
|
||||
|
||||
+1
-4
@@ -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>;
|
||||
}
|
||||
|
||||
+111
@@ -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,
|
||||
};
|
||||
+90
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
+24
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user