Merge pull request #186 from gaearon/more-suspensey-stuff

Fix more Suspense issues
This commit is contained in:
Dan Abramov
2019-04-19 13:18:50 +01:00
committed by GitHub
8 changed files with 1647 additions and 210 deletions
+8 -4
View File
@@ -69,16 +69,20 @@ For example, adding a function component `<Foo>` with an id 2:
#### Removing a node
Removing a fiber from the tree (a root or a leaf) only requires sending 2 numbers:
Removing a fiber from the tree (a root or a leaf) requires sending:
1. remove operation constant (`2`)
1. fiber id
1. how many items were removed
1. number of children
* (followed by a children-first list of removed fiber ids)
For example, removing a root fiber with an id of 1:
For example, removing fibers with ids of 35 and 21:
```js
[
2, // remove operation
1, // fiber id
1, // number of removed fibers
35, // first removed id
21, // second removed id
]
```
@@ -115,6 +115,260 @@ exports[`StoreStress (Sync Mode) should handle a stress test for Suspense (Sync
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 1`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key="a">
<Z>
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 2`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key="a">
<Z>
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 3`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key="a">
<B key="b">
<C key="c">
<Z>
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 4`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<C key="c">
<B key="b">
<A key="a">
<Z>
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 5`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<C key="c">
<A key="a">
<Z>
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 6`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<C key="c">
<A key="a">
<Z>
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 7`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<C key="c">
<A key="a">
<Z>
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 8`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key="a">
<B key="b">
<Z>
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 9`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key="a">
<Z>
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 10`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<Z>
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 11`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<B key="b">
<Z>
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 12`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key="a">
<Z>
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 13`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 14`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 15`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<B key="b">
<C key="c">
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 16`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<B key="b">
<A key="a">
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 17`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<A key="a">
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 18`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<A key="a">
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 19`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<A key="a">
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 20`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<B key="b">
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 21`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 22`] = `
[root]
▾ <Root>
<X>
<Suspense>
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 23`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<B key="b">
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test for Suspense without type change (Sync Mode) 24`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>
`;
exports[`StoreStress (Sync Mode) should handle a stress test with different tree operations (Sync Mode): 1: abcde 1`] = `
[root]
▾ <Parent>
@@ -115,6 +115,260 @@ exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurr
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 1`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key="a">
<Z>
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 2`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key="a">
<Z>
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 3`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key="a">
<B key="b">
<C key="c">
<Z>
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 4`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<C key="c">
<B key="b">
<A key="a">
<Z>
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 5`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<C key="c">
<A key="a">
<Z>
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 6`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<C key="c">
<A key="a">
<Z>
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 7`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<C key="c">
<A key="a">
<Z>
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 8`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key="a">
<B key="b">
<Z>
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 9`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key="a">
<Z>
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 10`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<Z>
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 11`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<B key="b">
<Z>
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 12`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
▾ <MaybeSuspend>
<A key="a">
<Z>
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 13`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 14`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 15`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<B key="b">
<C key="c">
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 16`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<B key="b">
<A key="a">
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 17`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<A key="a">
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 18`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<A key="a">
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 19`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<A key="a">
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 20`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<B key="b">
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 21`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 22`] = `
[root]
▾ <Root>
<X>
<Suspense>
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 23`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<B key="b">
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 24`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>
`;
exports[`StoreStressConcurrent should handle a stress test with different tree operations (Concurrent Mode): 1: abcde 1`] = `
[root]
▾ <Parent>
+436
View File
@@ -663,4 +663,440 @@ describe('StoreStress (Sync Mode)', () => {
}
}
});
it('should handle a stress test for Suspense without type change (Sync Mode)', () => {
const A = () => 'a';
const B = () => 'b';
const C = () => 'c';
const X = () => 'x';
const Y = () => 'y';
const Z = () => 'z';
const a = <A key="a" />;
const b = <B key="b" />;
const c = <C key="c" />;
const z = <Z key="z" />;
// prettier-ignore
const steps = [
a,
[a],
[a, b, c],
[c, b, a],
[c, null, a],
<React.Fragment>{c}{a}</React.Fragment>,
<div>{c}{a}</div>,
<div><span>{a}</span>{b}</div>,
[[a]],
null,
b,
a
];
const Never = () => {
throw new Promise(() => {});
};
const MaybeSuspend = ({ children, suspend }) => {
if (suspend) {
return (
<div>
{children}
<Never />
<X />
</div>
);
}
return (
<div>
{children}
<Z />
</div>
);
};
const Root = ({ children }) => {
return children;
};
// 1. For each step, check Suspense can render them as initial primary content.
// This is the only step where we use Jest snapshots.
let snapshots = [];
let container = document.createElement('div');
for (let i = 0; i < steps.length; i++) {
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
container
)
);
// We snapshot each step once so it doesn't regress.
expect(store).toMatchSnapshot();
snapshots.push(print(store));
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
}
// 2. Verify check Suspense can render same steps as initial fallback content.
// We don't actually assert here because the tree includes <MaybeSuspend>
// which is different from the snapshots above. So we take more snapshots.
let fallbackSnapshots = [];
for (let i = 0; i < steps.length; i++) {
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<Z />
<MaybeSuspend suspend={true}>{steps[i]}</MaybeSuspend>
<Z />
</React.Suspense>
<Y />
</Root>,
container
)
);
// We snapshot each step once so it doesn't regress.
expect(store).toMatchSnapshot();
fallbackSnapshots.push(print(store));
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
}
// 3. Verify we can update from each step to each step in primary mode.
for (let i = 0; i < steps.length; i++) {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
container
)
);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[j]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
container
)
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
container
)
);
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
}
}
// 4. Verify we can update from each step to each step in fallback mode.
for (let i = 0; i < steps.length; i++) {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<Z />
<MaybeSuspend suspend={true}>
<X />
<Y />
</MaybeSuspend>
<Z />
</React.Suspense>
<Y />
</Root>,
container
)
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Re-render with steps[j].
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<Z />
<MaybeSuspend suspend={true}>
<Y />
<X />
</MaybeSuspend>
<Z />
</React.Suspense>
<Y />
</Root>,
container
)
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Check that we can transition back again.
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<Z />
<MaybeSuspend suspend={true}>
<X />
<Y />
</MaybeSuspend>
<Z />
</React.Suspense>
<Y />
</Root>,
container
)
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Clean up after every iteration.
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
}
}
// 5. Verify we can update from each step to each step when moving primary -> fallback.
for (let i = 0; i < steps.length; i++) {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
container
)
);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<MaybeSuspend suspend={true}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
container
)
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Check that we can transition back again.
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
container
)
);
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
}
}
// 6. Verify we can update from each step to each step when moving fallback -> primary.
for (let i = 0; i < steps.length; i++) {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<MaybeSuspend suspend={true}>{steps[j]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
container
)
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Re-render with steps[j].
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<MaybeSuspend suspend={false}>{steps[j]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
container
)
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<MaybeSuspend suspend={true}>{steps[j]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
container
)
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Clean up after every iteration.
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
}
}
// 7. Verify we can update from each step to each step when toggling Suspense.
for (let i = 0; i < steps.length; i++) {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
container
)
);
// We get ID from the index in the tree above:
// Root, X, Suspense, ...
// ^ (index is 2)
const suspenseID = store.getElementIDAtIndex(2);
// Force fallback.
expect(print(store)).toEqual(snapshots[i]);
act(() => {
const suspenseID = store.getElementIDAtIndex(2);
bridge.send('overrideSuspense', {
id: suspenseID,
rendererID: store.getRendererIDForElement(suspenseID),
forceFallback: true,
});
});
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Stop forcing fallback.
act(() => {
bridge.send('overrideSuspense', {
id: suspenseID,
rendererID: store.getRendererIDForElement(suspenseID),
forceFallback: false,
});
});
expect(print(store)).toEqual(snapshots[i]);
// Trigger actual fallback.
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<MaybeSuspend suspend={true}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
container
)
);
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Force fallback while we're in fallback mode.
act(() => {
bridge.send('overrideSuspense', {
id: suspenseID,
rendererID: store.getRendererIDForElement(suspenseID),
forceFallback: true,
});
});
// Keep seeing fallback content.
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Switch to primary mode.
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>,
container
)
);
// Fallback is still forced though.
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Stop forcing fallback. This reverts to primary content.
act(() => {
bridge.send('overrideSuspense', {
id: suspenseID,
rendererID: store.getRendererIDForElement(suspenseID),
forceFallback: false,
});
});
// Now we see primary content.
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
}
}
});
});
@@ -668,4 +668,435 @@ describe('StoreStressConcurrent', () => {
}
}
});
it('should handle a stress test for Suspense without type change (Concurrent Mode)', () => {
const A = () => 'a';
const B = () => 'b';
const C = () => 'c';
const X = () => 'x';
const Y = () => 'y';
const Z = () => 'z';
const a = <A key="a" />;
const b = <B key="b" />;
const c = <C key="c" />;
const z = <Z key="z" />;
// prettier-ignore
const steps = [
a,
[a],
[a, b, c],
[c, b, a],
[c, null, a],
<React.Fragment>{c}{a}</React.Fragment>,
<div>{c}{a}</div>,
<div><span>{a}</span>{b}</div>,
[[a]],
null,
b,
a
];
const Never = () => {
throw new Promise(() => {});
};
const MaybeSuspend = ({ children, suspend }) => {
if (suspend) {
return (
<div>
{children}
<Never />
<X />
</div>
);
}
return (
<div>
{children}
<Z />
</div>
);
};
const Root = ({ children }) => {
return children;
};
// 1. For each step, check Suspense can render them as initial primary content.
// This is the only step where we use Jest snapshots.
let snapshots = [];
let container = document.createElement('div');
// $FlowFixMe
let root = ReactDOM.unstable_createRoot(container);
for (let i = 0; i < steps.length; i++) {
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>
)
);
// We snapshot each step once so it doesn't regress.
expect(store).toMatchSnapshot();
snapshots.push(print(store));
act(() => root.unmount());
expect(print(store)).toBe('');
}
// 2. Verify check Suspense can render same steps as initial fallback content.
// We don't actually assert here because the tree includes <MaybeSuspend>
// which is different from the snapshots above. So we take more snapshots.
let fallbackSnapshots = [];
for (let i = 0; i < steps.length; i++) {
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<Z />
<MaybeSuspend suspend={true}>{steps[i]}</MaybeSuspend>
<Z />
</React.Suspense>
<Y />
</Root>
)
);
// We snapshot each step once so it doesn't regress.
expect(store).toMatchSnapshot();
fallbackSnapshots.push(print(store));
act(() => root.unmount());
expect(print(store)).toBe('');
}
// 3. Verify we can update from each step to each step in primary mode.
for (let i = 0; i < steps.length; i++) {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
// $FlowFixMe
root = ReactDOM.unstable_createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>
)
);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[j]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>
)
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>
)
);
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => root.unmount());
expect(print(store)).toBe('');
}
}
// 4. Verify we can update from each step to each step in fallback mode.
for (let i = 0; i < steps.length; i++) {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
// $FlowFixMe
root = ReactDOM.unstable_createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<Z />
<MaybeSuspend suspend={true}>
<X />
<Y />
</MaybeSuspend>
<Z />
</React.Suspense>
<Y />
</Root>
)
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Re-render with steps[j].
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<Z />
<MaybeSuspend suspend={true}>
<Y />
<X />
</MaybeSuspend>
<Z />
</React.Suspense>
<Y />
</Root>
)
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Check that we can transition back again.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<Z />
<MaybeSuspend suspend={true}>
<X />
<Y />
</MaybeSuspend>
<Z />
</React.Suspense>
<Y />
</Root>
)
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Clean up after every iteration.
act(() => root.unmount());
expect(print(store)).toBe('');
}
}
// 5. Verify we can update from each step to each step when moving primary -> fallback.
for (let i = 0; i < steps.length; i++) {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
// $FlowFixMe
root = ReactDOM.unstable_createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>
)
);
expect(print(store)).toEqual(snapshots[i]);
// Re-render with steps[j].
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<MaybeSuspend suspend={true}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>
)
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Check that we can transition back again.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={z}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>
)
);
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => root.unmount());
expect(print(store)).toBe('');
}
}
// 6. Verify we can update from each step to each step when moving fallback -> primary.
for (let i = 0; i < steps.length; i++) {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
// $FlowFixMe
root = ReactDOM.unstable_createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<MaybeSuspend suspend={true}>{steps[j]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>
)
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Re-render with steps[j].
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<MaybeSuspend suspend={false}>{steps[j]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>
)
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Check that we can transition back again.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<MaybeSuspend suspend={true}>{steps[j]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>
)
);
expect(print(store)).toEqual(fallbackSnapshots[i]);
// Clean up after every iteration.
act(() => root.unmount());
expect(print(store)).toBe('');
}
}
// 7. Verify we can update from each step to each step when toggling Suspense.
for (let i = 0; i < steps.length; i++) {
for (let j = 0; j < steps.length; j++) {
// Always start with a fresh container and steps[i].
container = document.createElement('div');
// $FlowFixMe
root = ReactDOM.unstable_createRoot(container);
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>
)
);
// We get ID from the index in the tree above:
// Root, X, Suspense, ...
// ^ (index is 2)
const suspenseID = store.getElementIDAtIndex(2);
// Force fallback.
expect(print(store)).toEqual(snapshots[i]);
act(() => {
const suspenseID = store.getElementIDAtIndex(2);
bridge.send('overrideSuspense', {
id: suspenseID,
rendererID: store.getRendererIDForElement(suspenseID),
forceFallback: true,
});
});
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Stop forcing fallback.
act(() => {
bridge.send('overrideSuspense', {
id: suspenseID,
rendererID: store.getRendererIDForElement(suspenseID),
forceFallback: false,
});
});
expect(print(store)).toEqual(snapshots[i]);
// Trigger actual fallback.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<MaybeSuspend suspend={true}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>
)
);
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Force fallback while we're in fallback mode.
act(() => {
bridge.send('overrideSuspense', {
id: suspenseID,
rendererID: store.getRendererIDForElement(suspenseID),
forceFallback: true,
});
});
// Keep seeing fallback content.
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Switch to primary mode.
act(() =>
root.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>
<MaybeSuspend suspend={false}>{steps[i]}</MaybeSuspend>
</React.Suspense>
<Y />
</Root>
)
);
// Fallback is still forced though.
expect(print(store)).toEqual(fallbackSnapshots[j]);
// Stop forcing fallback. This reverts to primary content.
act(() => {
bridge.send('overrideSuspense', {
id: suspenseID,
rendererID: store.getRendererIDForElement(suspenseID),
forceFallback: false,
});
});
// Now we see primary content.
expect(print(store)).toEqual(snapshots[i]);
// Clean up after every iteration.
act(() => root.unmount());
expect(print(store)).toBe('');
}
}
});
});
+107 -42
View File
@@ -574,13 +574,21 @@ export function attach(
}
let pendingOperations: Array<number> = [];
let pendingOperationsQueue: Array<Array<number>> | null = [];
let pendingRealUnmountedIDs: Array<number> = [];
let pendingSimulatedUnmountedIDs: Array<number> = [];
let pendingOperationsQueue: Array<Uint32Array> | null = [];
// We keep track of which Fibers have been reported as unmounted by React
// during this commit phase so that we don't try to "hide" them or their
// children when Suspense flips to fallback. These Fibers won't have IDs.
let fibersUnmountedInThisCommitPhase: WeakSet<Fiber> = new WeakSet();
// TODO: we could make this layer DEV-only and write directly to pendingOperations.
let nextOperation: Array<number> = [];
function beginNextOperation(size: number): void {
nextOperation.length = size;
}
function endNextOperation(addToStartOfQueue: boolean): void {
function endNextOperation(): void {
if (__DEV__) {
for (let i = 0; i < nextOperation.length; i++) {
if (!Number.isInteger(nextOperation[i])) {
@@ -591,20 +599,16 @@ export function attach(
}
}
}
if (addToStartOfQueue) {
pendingOperations.splice.apply(
pendingOperations,
[0, 0].concat(nextOperation)
);
} else {
pendingOperations.push.apply(pendingOperations, nextOperation);
}
pendingOperations.push.apply(pendingOperations, nextOperation);
nextOperation.length = 0;
}
function flushPendingEvents(root: Object): void {
if (pendingOperations.length === 0) {
if (
pendingOperations.length === 0 &&
pendingRealUnmountedIDs.length === 0 &&
pendingSimulatedUnmountedIDs.length === 0
) {
// If we're currently profiling, send an "operations" method even if there are no mutations to the tree.
// The frontend needs this no-op info to know how to reconstruct the tree for each commit,
// even if a particular commit didn't change the shape of the tree.
@@ -613,13 +617,44 @@ export function attach(
}
}
const ops = new Uint32Array(
// Identify which renderer this update is coming from.
2 + // [rendererID, rootFiberID]
// All unmounts are batched in a single message.
2 + // [TREE_OPERATION_REMOVE, removedIDLength]
pendingRealUnmountedIDs.length +
pendingSimulatedUnmountedIDs.length +
// Regular operations
pendingOperations.length
);
// Identify which renderer this update is coming from.
// This enables roots to be mapped to renderers,
// Which in turn enables fiber props, states, and hooks to be inspected.
beginNextOperation(2);
nextOperation[0] = rendererID;
nextOperation[1] = getFiberID(getPrimaryFiber(root.current));
endNextOperation(true);
let i = 0;
ops[i++] = rendererID;
ops[i++] = getFiberID(getPrimaryFiber(root.current));
// All unmounts except roots are batched in a single message.
ops[i++] = TREE_OPERATION_REMOVE;
// The first number is how many unmounted IDs we're gonna send.
ops[i++] =
pendingRealUnmountedIDs.length + pendingSimulatedUnmountedIDs.length;
// Fill in the real unmounts in the reverse order.
// They were inserted parents-first by React, but we want children-first.
// So we traverse our array backwards.
for (let j = pendingRealUnmountedIDs.length - 1; j >= 0; j--) {
ops[i++] = pendingRealUnmountedIDs[j];
}
// Fill in the simulated unmounts (hidden Suspense subtrees) in their order.
// (We want children to go before parents.)
// They go *after* the real unmounts because we know for sure they won't be
// children of already pushed "real" IDs. If they were, we wouldn't be able
// to discover them during the traversal, as they would have been deleted.
ops.set(pendingSimulatedUnmountedIDs, i);
i += pendingSimulatedUnmountedIDs.length;
// Fill in the rest of the operations.
ops.set(pendingOperations, i);
// Let the frontend know about tree operations.
// The first value in this array will identify which root it corresponds to,
@@ -628,13 +663,16 @@ export function attach(
// Until the frontend has been connected, store the tree operations.
// This will let us avoid walking the tree later when the frontend connects,
// and it enables the Profiler's reload-and-profile functionality to work as well.
pendingOperationsQueue.push(pendingOperations);
pendingOperationsQueue.push(ops);
} else {
// If we've already connected to the frontend, just pass the operations through.
hook.emit('operations', Uint32Array.from(pendingOperations));
hook.emit('operations', ops);
}
pendingOperations = [];
pendingOperations.length = 0;
pendingRealUnmountedIDs.length = 0;
pendingSimulatedUnmountedIDs.length = 0;
fibersUnmountedInThisCommitPhase = new WeakSet();
}
function recordMount(fiber: Fiber, parentFiber: Fiber | null) {
@@ -656,7 +694,7 @@ export function attach(
nextOperation[2] = ElementTypeRoot;
nextOperation[3] = isProfilingSupported ? 1 : 0;
nextOperation[4] = hasOwnerMetadata ? 1 : 0;
endNextOperation(false);
endNextOperation();
} else {
const { displayName, key, type } = getDataForFiber(fiber);
const { _debugOwner } = fiber;
@@ -701,7 +739,7 @@ export function attach(
nextOperation[6 + encodedDisplayNameSize + 1 + i] = encodedKey[i];
}
}
endNextOperation(false);
endNextOperation();
}
if (isProfiling) {
@@ -713,7 +751,7 @@ export function attach(
nextOperation[0] = TREE_OPERATION_UPDATE_TREE_BASE_DURATION;
nextOperation[1] = id;
nextOperation[2] = treeBaseDuration;
endNextOperation(false);
endNextOperation();
const { actualDuration } = fiber;
if (actualDuration > 0) {
@@ -728,7 +766,7 @@ export function attach(
}
}
function recordUnmount(fiber: Fiber) {
function recordUnmount(fiber: Fiber, isSimulated: boolean) {
const isRoot = fiber.tag === HostRoot;
const primaryFiber = getPrimaryFiber(fiber);
if (!fiberToIDMap.has(primaryFiber)) {
@@ -743,19 +781,22 @@ export function attach(
}
const id = getFiberID(primaryFiber);
if (isRoot) {
beginNextOperation(2);
// Removing a root needs to happen at the end
// so we don't batch it with other unmounts.
beginNextOperation(3);
nextOperation[0] = TREE_OPERATION_REMOVE;
nextOperation[1] = id;
endNextOperation(false);
nextOperation[1] = 1; // Remove one item
nextOperation[2] = id;
endNextOperation();
} else if (!shouldFilterFiber(fiber)) {
beginNextOperation(2);
nextOperation[0] = TREE_OPERATION_REMOVE;
nextOperation[1] = id;
// Non-root fibers are deleted during the commit phase.
// They are deleted in the parent-first order. However
// DevTools currently expects deletions to be child-first.
// This is why we prepend the delete operation to the queue.
endNextOperation(true);
// To maintain child-first ordering,
// we'll push it into one of these queues,
// and later arrange them in the correct order.
if (isSimulated) {
pendingSimulatedUnmountedIDs.push(id);
} else {
pendingRealUnmountedIDs.push(id);
}
}
fiberToIDMap.delete(primaryFiber);
idToFiberMap.delete(id);
@@ -815,14 +856,35 @@ export function attach(
}
}
// We use this to simulate unmounting for Suspense trees
// when we switch from primary to fallback.
function unmountFiberChildrenRecursively(fiber: Fiber) {
if (__DEBUG__) {
debug('unmountFiberChildrenRecursively()', fiber);
}
// We might meet a nested Suspense on our way.
const isTimedOutSuspense =
fiber.tag === ReactTypeOfWork.SuspenseComponent &&
fiber.memoizedState !== null;
let child = fiber.child;
if (isTimedOutSuspense) {
// If it's showing fallback tree, let's traverse it instead.
const primaryChildFragment = fiber.child;
const fallbackChildFragment = primaryChildFragment.sibling;
// Skip over to the real Fiber child.
child = fallbackChildFragment.child;
}
while (child !== null) {
recordUnmount(child);
unmountFiberChildrenRecursively(child);
// Record simulated unmounts children-first.
// We might find real committed unmounts along the way--skip them.
// Otherwise we would send duplicated messages for the same IDs.
if (!fibersUnmountedInThisCommitPhase.has(child)) {
unmountFiberChildrenRecursively(child);
recordUnmount(child, true);
}
child = child.sibling;
}
}
@@ -843,7 +905,7 @@ export function attach(
nextOperation[0] = TREE_OPERATION_UPDATE_TREE_BASE_DURATION;
nextOperation[1] = getFiberID(getPrimaryFiber(fiber));
nextOperation[2] = treeBaseDuration;
endNextOperation(false);
endNextOperation();
}
if (haveProfilerTimesChanged(fiber.alternate, fiber)) {
@@ -882,7 +944,7 @@ export function attach(
for (let i = 0; i < nextChildren.length; i++) {
nextOperation[3 + i] = nextChildren[i];
}
endNextOperation(false);
endNextOperation();
}
function findReorderedChildrenRecursively(
@@ -1058,8 +1120,8 @@ export function attach(
) {
// We may have already queued up some operations before the frontend connected
// If so, let the frontend know about them.
localPendingOperationsQueue.forEach(pendingOperations => {
hook.emit('operations', Uint32Array.from(pendingOperations));
localPendingOperationsQueue.forEach(ops => {
hook.emit('operations', ops);
});
} else {
// If we have not been profiling, then we can just walk the tree and build up its current state as-is.
@@ -1090,10 +1152,13 @@ export function attach(
}
function handleCommitFiberUnmount(fiber) {
// Remeber this is a real deletion so we don't
// go down this tree when hiding Suspense nodes.
fibersUnmountedInThisCommitPhase.add(fiber);
// This is not recursive.
// We can't traverse fibers after unmounting so instead
// we rely on React telling us about each unmount.
recordUnmount(fiber);
recordUnmount(fiber, false);
}
function handleCommitFiberRoot(root) {
@@ -1133,7 +1198,7 @@ export function attach(
updateFiberRecursively(current, alternate, null);
} else if (wasMounted && !isMounted) {
// Unmount an existing root.
recordUnmount(current);
recordUnmount(current, false);
}
} else {
// Mount a new root.
+115 -124
View File
@@ -543,6 +543,33 @@ export default class Store extends EventEmitter {
}
};
_adjustParentTreeWeight = (
parentElement: Element | null,
weightDelta: number
) => {
let isInsideCollapsedSubTree = false;
while (parentElement != null) {
parentElement.weight += weightDelta;
// Additions and deletions within a collapsed subtree should not bubble beyond the collapsed parent.
// Their weight will bubble up when the parent is expanded.
if (parentElement.isCollapsed) {
isInsideCollapsedSubTree = true;
break;
}
parentElement = ((this._idToElement.get(
parentElement.parentID
): any): Element);
}
// Additions and deletions within a collapsed subtree should not affect the overall number of elements.
if (!isInsideCollapsedSubTree) {
this._weightAcrossRoots += weightDelta;
}
};
onBridgeOperations = (operations: Uint32Array) => {
if (!(operations instanceof Uint32Array)) {
// $FlowFixMe TODO HACK Temporary workaround for the fact that Chrome is not transferring the typed array.
@@ -574,25 +601,16 @@ export default class Store extends EventEmitter {
}
}
let addedElementIDs: Uint32Array = new Uint32Array(0);
let removedElementIDs: Uint32Array = new Uint32Array(0);
const addedElementIDs: Array<number> = [];
const removedElementIDs: Array<number> = [];
let i = 2;
while (i < operations.length) {
let id: number = ((null: any): number);
let element: Element = ((null: any): Element);
let ownerID: number = 0;
let parentID: number = ((null: any): number);
let parentElement: Element = ((null: any): Element);
let type: ElementType = ((null: any): ElementType);
let weightDelta: number = 0;
const operation = operations[i];
switch (operation) {
case TREE_OPERATION_ADD:
id = ((operations[i + 1]: any): number);
type = ((operations[i + 2]: any): ElementType);
case TREE_OPERATION_ADD: {
const id = ((operations[i + 1]: any): number);
const type = ((operations[i + 2]: any): ElementType);
i = i + 3;
@@ -602,6 +620,8 @@ export default class Store extends EventEmitter {
);
}
let ownerID: number = 0;
let parentID: number = ((null: any): number);
if (type === ElementTypeRoot) {
if (__DEBUG__) {
debug('Add', `new root node ${id}`);
@@ -672,7 +692,9 @@ export default class Store extends EventEmitter {
);
}
parentElement = ((this._idToElement.get(parentID): any): Element);
const parentElement = ((this._idToElement.get(
parentID
): any): Element);
parentElement.children = parentElement.children.concat(id);
const element: Element = {
@@ -689,70 +711,67 @@ export default class Store extends EventEmitter {
};
this._idToElement.set(id, element);
const oldAddedElementIDs = addedElementIDs;
addedElementIDs = new Uint32Array(addedElementIDs.length + 1);
addedElementIDs.set(oldAddedElementIDs);
addedElementIDs[oldAddedElementIDs.length] = id;
weightDelta = 1;
addedElementIDs.push(id);
this._adjustParentTreeWeight(parentElement, 1);
}
break;
case TREE_OPERATION_REMOVE: {
id = ((operations[i + 1]: any): number);
if (!this._idToElement.has(id)) {
throw Error(
`Cannot remove node ${id} because no matching node was found in the Store.`
);
}
i = i + 2;
element = ((this._idToElement.get(id): any): Element);
parentID = element.parentID;
weightDelta = -element.weight;
if (element.children.length > 0) {
throw new Error(`Node ${id} was removed before its children.`);
}
this._idToElement.delete(id);
if (parentID === 0) {
if (__DEBUG__) {
debug('Remove', `node ${id} root`);
}
this._roots = this._roots.filter(rootID => rootID !== id);
this._rootIDToRendererID.delete(id);
this._rootIDToCapabilities.delete(id);
haveRootsChanged = true;
} else {
if (__DEBUG__) {
debug('Remove', `node ${id} from parent ${parentID}`);
}
parentElement = ((this._idToElement.get(parentID): any): Element);
if (parentElement === undefined) {
throw Error(
`Cannot remove node ${id} from parent ${parentID} because no matching node was found in the Store.`
);
}
parentElement.children = parentElement.children.filter(
childID => childID !== id
);
}
// Track removed items so search results can be updated
const oldRemovedElementIDs = removedElementIDs;
removedElementIDs = new Uint32Array(removedElementIDs.length + 1);
removedElementIDs.set(oldRemovedElementIDs);
removedElementIDs[oldRemovedElementIDs.length] = id;
break;
}
case TREE_OPERATION_REORDER_CHILDREN:
id = ((operations[i + 1]: any): number);
case TREE_OPERATION_REMOVE: {
const removeLength = ((operations[i + 1]: any): number);
i = i + 2;
for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
const id = ((operations[i]: any): number);
if (!this._idToElement.has(id)) {
throw Error(
`Cannot remove node ${id} because no matching node was found in the Store.`
);
}
i = i + 1;
const element = ((this._idToElement.get(id): any): Element);
if (element.children.length > 0) {
throw new Error(`Node ${id} was removed before its children.`);
}
this._idToElement.delete(id);
const parentID = element.parentID;
let parentElement = null;
if (parentID === 0) {
if (__DEBUG__) {
debug('Remove', `node ${id} root`);
}
this._roots = this._roots.filter(rootID => rootID !== id);
this._rootIDToRendererID.delete(id);
this._rootIDToCapabilities.delete(id);
haveRootsChanged = true;
} else {
if (__DEBUG__) {
debug('Remove', `node ${id} from parent ${parentID}`);
}
parentElement = ((this._idToElement.get(parentID): any): Element);
if (parentElement === undefined) {
throw Error(
`Cannot remove node ${id} from parent ${parentID} because no matching node was found in the Store.`
);
}
parentElement.children = parentElement.children.filter(
childID => childID !== id
);
}
this._adjustParentTreeWeight(parentElement, -element.weight);
removedElementIDs.push(id);
}
break;
}
case TREE_OPERATION_REORDER_CHILDREN: {
const id = ((operations[i + 1]: any): number);
const numChildren = ((operations[i + 2]: any): number);
const nextChildren = ((operations.slice(
i + 3,
@@ -771,38 +790,29 @@ export default class Store extends EventEmitter {
);
}
element = ((this._idToElement.get(id): any): Element);
const element = ((this._idToElement.get(id): any): Element);
const prevChildren = element.children;
if (
nextChildren.length !== prevChildren.length ||
nextChildren.find(childID => {
const childElement = this._idToElement.get(childID);
return childElement == null || childElement.parentID !== id;
}) != null
) {
if (nextChildren.length !== prevChildren.length) {
throw Error(
`Children cannot be added or removed during a reorder operation.`
);
}
element.children = Array.from(nextChildren);
if (!element.isCollapsed) {
const prevWeight = element.weight;
let nextWeight = element.type === ElementTypeRoot ? 0 : 1;
nextChildren.forEach(childID => {
const child = ((this._idToElement.get(childID): any): Element);
nextWeight += child.isCollapsed ? 1 : child.weight;
});
element.weight = nextWeight;
weightDelta = nextWeight - prevWeight;
// This check is more expensive so it's gated
if (__DEV__) {
if (
nextChildren.find(childID => {
const childElement = this._idToElement.get(childID);
return childElement == null || childElement.parentID !== id;
}) != null
) {
console.error(
`Children cannot be added or removed during a reorder operation.`
);
}
}
element.children = Array.from(nextChildren);
break;
}
case TREE_OPERATION_UPDATE_TREE_BASE_DURATION:
// Base duration updates are only sent while profiling is in progress.
// We can ignore them at this point.
@@ -812,28 +822,6 @@ export default class Store extends EventEmitter {
default:
throw Error(`Unsupported Bridge operation ${operation}`);
}
let isInsideCollapsedSubTree = false;
while (parentElement != null) {
parentElement.weight += weightDelta;
// Additions and deletions within a collapsed subtree should not bubble beyond the collapsed parent.
// Their weight will bubble up when the parent is expanded.
if (parentElement.isCollapsed) {
isInsideCollapsedSubTree = true;
break;
}
parentElement = ((this._idToElement.get(
parentElement.parentID
): any): Element);
}
// Additions and deletions within a collapsed subtree should not affect the overall number of elements.
if (!isInsideCollapsedSubTree) {
this._weightAcrossRoots += weightDelta;
}
}
this._revision++;
@@ -860,7 +848,10 @@ export default class Store extends EventEmitter {
console.groupEnd();
}
this.emit('mutated', [addedElementIDs, removedElementIDs]);
this.emit('mutated', [
new Uint32Array(addedElementIDs),
new Uint32Array(removedElementIDs),
]);
};
onProfilingStatus = (isProfiling: boolean) => {
@@ -168,18 +168,12 @@ function updateTree(
let i = 2;
while (i < operations.length) {
let id: number = ((null: any): number);
let node: Node = ((null: any): Node);
let parentID: number = ((null: any): number);
let parentNode: Node = ((null: any): Node);
let type: ElementType = ((null: any): ElementType);
const operation = operations[i];
switch (operation) {
case TREE_OPERATION_ADD:
id = ((operations[i + 1]: any): number);
type = ((operations[i + 2]: any): ElementType);
const id = ((operations[i + 1]: any): number);
const type = ((operations[i + 2]: any): ElementType);
i = i + 3;
@@ -210,7 +204,7 @@ function updateTree(
nodes.set(id, node);
} else {
parentID = ((operations[i]: any): number);
const parentID = ((operations[i]: any): number);
i++;
i++; // ownerID
@@ -240,7 +234,7 @@ function updateTree(
);
}
parentNode = getClonedNode(parentID);
const parentNode = getClonedNode(parentID);
parentNode.children = parentNode.children.concat(id);
const node: Node = {
@@ -254,40 +248,46 @@ function updateTree(
nodes.set(id, node);
}
break;
case TREE_OPERATION_REMOVE:
id = ((operations[i + 1]: any): number);
break;
case TREE_OPERATION_REMOVE: {
const removeLength = ((operations[i + 1]: any): number);
i = i + 2;
if (!nodes.has(id)) {
throw new Error(
'Commit tree does not contain fiber ' +
id +
'. This is a bug in React DevTools.'
);
}
for (let removeIndex = 0; removeIndex < removeLength; removeIndex++) {
const id = ((operations[i]: any): number);
i = i + 1;
node = getClonedNode(id);
parentID = node.parentID;
nodes.delete(id);
parentNode = getClonedNode(parentID);
if (parentNode == null) {
// No-op
} else {
if (__DEBUG__) {
debug('Remove', `fiber ${id} from parent ${parentID}`);
if (!nodes.has(id)) {
throw new Error(
'Commit tree does not contain fiber ' +
id +
'. This is a bug in React DevTools.'
);
}
parentNode.children = parentNode.children.filter(
childID => childID !== id
);
const node = getClonedNode(id);
const parentID = node.parentID;
nodes.delete(id);
const parentNode = getClonedNode(parentID);
if (parentNode == null) {
// No-op
} else {
if (__DEBUG__) {
debug('Remove', `fiber ${id} from parent ${parentID}`);
}
parentNode.children = parentNode.children.filter(
childID => childID !== id
);
}
}
break;
case TREE_OPERATION_REORDER_CHILDREN:
id = ((operations[i + 1]: any): number);
}
case TREE_OPERATION_REORDER_CHILDREN: {
const id = ((operations[i + 1]: any): number);
const numChildren = ((operations[i + 2]: any): number);
const children = ((operations.slice(
i + 3,
@@ -300,14 +300,15 @@ function updateTree(
debug('Re-order', `fiber ${id} children ${children.join(',')}`);
}
node = getClonedNode(id);
const node = getClonedNode(id);
node.children = Array.from(children);
break;
case TREE_OPERATION_UPDATE_TREE_BASE_DURATION:
id = operations[i + 1];
}
case TREE_OPERATION_UPDATE_TREE_BASE_DURATION: {
const id = operations[i + 1];
node = getClonedNode(id);
const node = getClonedNode(id);
node.treeBaseDuration = operations[i + 2] / 1000; // Convert microseconds back to milliseconds;
if (__DEBUG__) {
@@ -319,6 +320,7 @@ function updateTree(
i = i + 3;
break;
}
default:
throw Error(`Unsupported Bridge operation ${operation}`);
}