Merge pull request #164 from gaearon/fix-lots-of-stuff

Fix reordering and Suspense logic
This commit is contained in:
Dan Abramov
2019-04-16 19:11:28 +01:00
committed by GitHub
7 changed files with 1128 additions and 822 deletions
+3 -3
View File
@@ -130,10 +130,10 @@
"opener": "^1.5.1",
"prettier": "^1.16.4",
"prop-types": "^15.6.2",
"react": "^16.9.0-alpha.0",
"react": "0.0.0-4221565e1",
"react-color": "^2.11.7",
"react-dom": "^16.9.0-alpha.0",
"react-is": "^16.9.0-alpha.0",
"react-dom": "0.0.0-4221565e1",
"react-is": "0.0.0-4221565e1",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.0",
"request-promise": "^4.2.4",
@@ -25,142 +25,6 @@ exports[`Store should filter DOM nodes from the store tree: 1: mount 1`] = `
<Child>
`;
exports[`Store should handle a stress test for Suspense 1`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>
`;
exports[`Store should handle a stress test for Suspense 2`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>
`;
exports[`Store should handle a stress test for Suspense 3`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<B key="b">
<C key="c">
<Y>
`;
exports[`Store should handle a stress test for Suspense 4`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<B key="b">
<A key="a">
<Y>
`;
exports[`Store should handle a stress test for Suspense 5`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<A key="a">
<Y>
`;
exports[`Store should handle a stress test for Suspense 6`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<A key="a">
<Y>
`;
exports[`Store should handle a stress test for Suspense 7`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<A key="a">
<Y>
`;
exports[`Store should handle a stress test for Suspense 8`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<B key="b">
<Y>
`;
exports[`Store should handle a stress test for Suspense 9`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>
`;
exports[`Store should handle a stress test for Suspense 10`] = `
[root]
▾ <Root>
<X>
<Suspense>
<Y>
`;
exports[`Store should handle a stress test for Suspense 11`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<B key="b">
<Y>
`;
exports[`Store should handle a stress test for Suspense 12`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>
`;
exports[`Store should handle a stress test with different tree operations: 1: abcde 1`] = `
[root]
▾ <Parent>
<A key="a">
<B key="b">
<C key="c">
<D key="d">
<E key="e">
`;
exports[`Store should handle a stress test with different tree operations: 2: abxde 1`] = `
[root]
▾ <Parent>
<A key="a">
<B key="b">
▾ <C key="c">
<X>
<D key="d">
<E key="e">
`;
exports[`Store should support collapsing parts of the tree: 1: mount 1`] = `
[root]
▾ <Grandparent>
@@ -0,0 +1,239 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StoreStress should handle a stress test for Suspense 1`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>
`;
exports[`StoreStress should handle a stress test for Suspense 2`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>
`;
exports[`StoreStress should handle a stress test for Suspense 3`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<B key="b">
<C key="c">
<Y>
`;
exports[`StoreStress should handle a stress test for Suspense 4`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<B key="b">
<A key="a">
<Y>
`;
exports[`StoreStress should handle a stress test for Suspense 5`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<A key="a">
<Y>
`;
exports[`StoreStress should handle a stress test for Suspense 6`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<A key="a">
<Y>
`;
exports[`StoreStress should handle a stress test for Suspense 7`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<C key="c">
<A key="a">
<Y>
`;
exports[`StoreStress should handle a stress test for Suspense 8`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<B key="b">
<Y>
`;
exports[`StoreStress should handle a stress test for Suspense 9`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>
`;
exports[`StoreStress should handle a stress test for Suspense 10`] = `
[root]
▾ <Root>
<X>
<Suspense>
<Y>
`;
exports[`StoreStress should handle a stress test for Suspense 11`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<B key="b">
<Y>
`;
exports[`StoreStress should handle a stress test for Suspense 12`] = `
[root]
▾ <Root>
<X>
▾ <Suspense>
<A key="a">
<Y>
`;
exports[`StoreStress should handle a stress test with different tree operations: 1: abcde 1`] = `
[root]
▾ <Parent>
<A key="a">
<B key="b">
<C key="c">
<D key="d">
<E key="e">
`;
exports[`StoreStress should handle a stress test with different tree operations: 2: abxde 1`] = `
[root]
▾ <Parent>
<A key="a">
<B key="b">
▾ <C key="c">
<X>
<D key="d">
<E key="e">
`;
exports[`StoreStress should handle stress test with reordering 1`] = `
[root]
▾ <Root>
<A key="a">
`;
exports[`StoreStress should handle stress test with reordering 2`] = `
[root]
▾ <Root>
<B key="b">
`;
exports[`StoreStress should handle stress test with reordering 3`] = `
[root]
▾ <Root>
<C key="c">
`;
exports[`StoreStress should handle stress test with reordering 4`] = `
[root]
▾ <Root>
<D key="d">
`;
exports[`StoreStress should handle stress test with reordering 5`] = `
[root]
▾ <Root>
<E key="e">
`;
exports[`StoreStress should handle stress test with reordering 6`] = `
[root]
▾ <Root>
<A key="a">
`;
exports[`StoreStress should handle stress test with reordering 7`] = `
[root]
▾ <Root>
<B key="b">
`;
exports[`StoreStress should handle stress test with reordering 8`] = `
[root]
▾ <Root>
<C key="c">
`;
exports[`StoreStress should handle stress test with reordering 9`] = `
[root]
▾ <Root>
<D key="d">
`;
exports[`StoreStress should handle stress test with reordering 10`] = `
[root]
▾ <Root>
<E key="e">
`;
exports[`StoreStress should handle stress test with reordering 11`] = `
[root]
▾ <Root>
<A key="a">
<B key="b">
`;
exports[`StoreStress should handle stress test with reordering 12`] = `
[root]
▾ <Root>
<B key="b">
<A key="a">
`;
exports[`StoreStress should handle stress test with reordering 13`] = `
[root]
▾ <Root>
<B key="b">
<C key="c">
`;
exports[`StoreStress should handle stress test with reordering 14`] = `
[root]
▾ <Root>
<C key="c">
<B key="b">
`;
exports[`StoreStress should handle stress test with reordering 15`] = `
[root]
▾ <Root>
<A key="a">
<C key="c">
`;
exports[`StoreStress should handle stress test with reordering 16`] = `
[root]
▾ <Root>
<C key="c">
<A key="a">
`;
+2 -519
View File
@@ -4,9 +4,7 @@ describe('Store', () => {
let React;
let ReactDOM;
let TestUtils;
// let bridge;
let store;
let print;
const act = (callback: Function) => {
TestUtils.act(() => {
@@ -16,14 +14,11 @@ describe('Store', () => {
};
beforeEach(() => {
// bridge = global.bridge;
store = global.store;
React = require('react');
ReactDOM = require('react-dom');
TestUtils = require('react-dom/test-utils');
print = require('./storeSerializer').print;
});
it('should support mount and update operations', () => {
@@ -98,8 +93,7 @@ describe('Store', () => {
expect(store).toMatchSnapshot('1: mount');
});
// TODO We should write more complex Suspense tests than just this
it('should display Suspense nodes properly in various states', async done => {
it('should display Suspense nodes properly in various states', () => {
const Loading = () => <div>Loading...</div>;
const SuspendingComponent = () => {
throw new Promise(() => {});
@@ -124,12 +118,10 @@ describe('Store', () => {
act(() => ReactDOM.render(<Wrapper shouldSuspense={true} />, container));
expect(store).toMatchSnapshot('1: loading');
act(async () => {
act(() => {
ReactDOM.render(<Wrapper shouldSuspense={false} />, container);
});
expect(store).toMatchSnapshot('2: resolved');
done();
});
it('should support collapsing parts of the tree', () => {
@@ -167,513 +159,4 @@ describe('Store', () => {
act(() => store.toggleIsCollapsed(grandparentID, false));
expect(store).toMatchSnapshot('6: expand Grandparent');
});
// This is a stress test for the tree mount/update/unmount traversal.
// It renders different trees that should produce the same output.
it('should handle a stress test with different tree operations', () => {
let setShowX;
const A = () => 'a';
const B = () => 'b';
const C = () => {
// We'll be manually flipping this component back and forth in the test.
// We only do this for a single node in order to verify that DevTools
// can handle a subtree switching alternates while other subtrees are memoized.
let [showX, _setShowX] = React.useState(false);
setShowX = _setShowX;
return showX ? <X /> : 'c';
};
const D = () => 'd';
const E = () => 'e';
const X = () => 'x';
const a = <A key="a" />;
const b = <B key="b" />;
const c = <C key="c" />;
const d = <D key="d" />;
const e = <E key="e" />;
function Parent({ children }) {
return children;
}
// 1. Render a normal version of [a, b, c, d, e].
let container = document.createElement('div');
act(() => ReactDOM.render(<Parent>{[a, b, c, d, e]}</Parent>, container));
expect(store).toMatchSnapshot('1: abcde');
expect(container.textContent).toMatch('abcde');
const snapshotForABCDE = print(store);
// 2. Render a version where <C /> renders an <X /> child instead of 'c'.
// This is how we'll test an update to a single component.
act(() => {
setShowX(true);
});
expect(store).toMatchSnapshot('2: abxde');
expect(container.textContent).toMatch('abxde');
const snapshotForABXDE = print(store);
// 3. Verify flipping it back produces the original result.
act(() => {
setShowX(false);
});
expect(container.textContent).toMatch('abcde');
expect(print(store)).toBe(snapshotForABCDE);
// 4. Clean up.
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
// Now comes the interesting part.
// All of these cases are equivalent to [a, b, c, d, e] in output.
// We'll verify that DevTools produces the same snapshots for them.
// These cases are picked so that rendering them sequentially in the same
// container results in a combination of mounts, updates, unmounts, and reorders.
// prettier-ignore
let cases = [
[a, b, c, d, e],
[[a], b, c, d, e],
[[a, b], c, d, e],
[[a, b], c, [d, e]],
[[a, b], c, [d, '', e]],
[[a], b, c, d, [e]],
[a, b, [[c]], d, e],
[[a, ''], [b], [c], [d], [e]],
[a, b, [c, [d, ['', e]]]],
[a, b, c, d, e],
[<div key="0">{a}</div>, b, c, d, e],
[<div key="0">{a}{b}</div>, c, d, e],
[<div key="0">{a}{b}</div>, c, <div key="1">{d}{e}</div>],
[<div key="1">{a}{b}</div>, c, <div key="0">{d}{e}</div>],
[<div key="0">{a}{b}</div>, c, <div key="1">{d}{e}</div>],
[<div key="2">{a}{b}</div>, c, <div key="3">{d}{e}</div>],
[<span key="0">{a}</span>, b, c, d, [e]],
[a, b, <span key="0"><span>{c}</span></span>, d, e],
[<div key="0">{a}</div>, [b], <span key="1">{c}</span>, [d], <div key="2">{e}</div>],
[a, b, [c, <div key="0">{d}<span>{e}</span></div>], ''],
[a, [[]], b, c, [d, [[]], e]],
[[[a, b, c, d], e]],
[a, b, c, d, e]
];
// 5. Test fresh mount for each case.
for (let i = 0; i < cases.length; i++) {
// Ensure fresh mount.
container = document.createElement('div');
// Verify mounting 'abcde'.
act(() => ReactDOM.render(<Parent>{cases[i]}</Parent>, container));
expect(container.textContent).toMatch('abcde');
expect(print(store)).toEqual(snapshotForABCDE);
// Verify switching to 'abxde'.
act(() => {
setShowX(true);
});
expect(container.textContent).toMatch('abxde');
expect(print(store)).toBe(snapshotForABXDE);
// Verify switching back to 'abcde'.
act(() => {
setShowX(false);
});
expect(container.textContent).toMatch('abcde');
expect(print(store)).toBe(snapshotForABCDE);
// Clean up.
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
}
// 6. Verify *updates* by reusing the container between iterations.
// There'll be no unmounting until the very end.
container = document.createElement('div');
for (let i = 0; i < cases.length; i++) {
// Verify mounting 'abcde'.
act(() => ReactDOM.render(<Parent>{cases[i]}</Parent>, container));
expect(container.textContent).toMatch('abcde');
expect(print(store)).toEqual(snapshotForABCDE);
// Verify switching to 'abxde'.
act(() => {
setShowX(true);
});
expect(container.textContent).toMatch('abxde');
expect(print(store)).toBe(snapshotForABXDE);
// Verify switching back to 'abcde'.
act(() => {
setShowX(false);
});
expect(container.textContent).toMatch('abcde');
expect(print(store)).toBe(snapshotForABCDE);
// Don't unmount. Reuse the container between iterations.
}
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
// 7. Same as the previous step, but for Concurrent Mode.
container = document.createElement('div');
// $FlowFixMe
let root = ReactDOM.unstable_createRoot(container);
for (let i = 0; i < cases.length; i++) {
// Verify mounting 'abcde'.
act(() => root.render(<Parent>{cases[i]}</Parent>));
expect(container.textContent).toMatch('abcde');
expect(print(store)).toEqual(snapshotForABCDE);
// Verify switching to 'abxde'.
act(() => {
setShowX(true);
});
expect(container.textContent).toMatch('abxde');
expect(print(store)).toBe(snapshotForABXDE);
// Verify switching back to 'abcde'.
act(() => {
setShowX(false);
});
expect(container.textContent).toMatch('abcde');
expect(print(store)).toBe(snapshotForABCDE);
// Don't unmount. Reuse the container between iterations.
}
act(() => root.unmount());
expect(print(store)).toBe('');
});
it('should handle a stress test for Suspense', async () => {
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 x = <X key="x" />;
// const y = <Y key="y" />;
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 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}>{steps[i]}</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.
for (let i = 0; i < steps.length; i++) {
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<Z />
<Never />
<Z />
</React.Suspense>
<Y />
</Root>,
container
)
);
expect(print(store)).toEqual(snapshots[i]);
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}>{steps[i]}</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}>{steps[j]}</React.Suspense>
<Y />
</Root>,
container
)
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Clean up after every iteration.
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
}
}
// TODO: fix the bugs
// // 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 />
// <Never />
// <Z />
// </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]}>
// <Z />
// <Never />
// <Z />
// </React.Suspense>
// <Y />
// </Root>,
// container
// ));
// // Verify the successful transition to steps[j].
// expect(print(store)).toEqual(snapshots[j]);
// // 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}>{steps[i]}</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]}>
<Z />
<Never />
<Z />
</React.Suspense>
<Y />
</Root>,
container
)
);
// Verify the successful transition to steps[j].
expect(print(store)).toEqual(snapshots[j]);
// Clean up after every iteration.
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
}
}
// TODO: fix the bugs
// 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]}>
// <Z />
// <Never />
// <Z />
// </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}>
// {steps[j]}
// </React.Suspense>
// <Y />
// </Root>,
// container
// ));
// // Verify the successful transition to steps[j].
// expect(print(store)).toEqual(snapshots[j]);
// // Clean up after every iteration.
// act(() => ReactDOM.unmountComponentAtNode(container));
// expect(print(store)).toBe('');
// }
// }
// TODO: fix the bugs
// 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]}>
// {steps[i]}
// </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(snapshots[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]}>
// <Z />
// <Never />
// <Z />
// </React.Suspense>
// <Y />
// </Root>,
// container
// ));
// expect(print(store)).toEqual(snapshots[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(snapshots[j]);
// // Switch to primary mode.
// act(() => ReactDOM.render(
// <Root>
// <X />
// <React.Suspense fallback={steps[j]}>
// {steps[i]}
// </React.Suspense>
// <Y />
// </Root>,
// container
// ));
// // Fallback is still forced though.
// expect(print(store)).toEqual(snapshots[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('');
// }
// }
// TODO:
// Test Concurrent Mode
});
});
+694
View File
@@ -0,0 +1,694 @@
// @flow
describe('StoreStress', () => {
let React;
let ReactDOM;
let TestUtils;
let bridge;
let store;
let print;
const act = (callback: Function) => {
TestUtils.act(() => {
callback();
});
jest.runAllTimers(); // Flush Bridge operations
};
beforeEach(() => {
bridge = global.bridge;
store = global.store;
React = require('react');
ReactDOM = require('react-dom');
TestUtils = require('react-dom/test-utils');
print = require('./storeSerializer').print;
});
// This is a stress test for the tree mount/update/unmount traversal.
// It renders different trees that should produce the same output.
it('should handle a stress test with different tree operations', () => {
let setShowX;
const A = () => 'a';
const B = () => 'b';
const C = () => {
// We'll be manually flipping this component back and forth in the test.
// We only do this for a single node in order to verify that DevTools
// can handle a subtree switching alternates while other subtrees are memoized.
let [showX, _setShowX] = React.useState(false);
setShowX = _setShowX;
return showX ? <X /> : 'c';
};
const D = () => 'd';
const E = () => 'e';
const X = () => 'x';
const a = <A key="a" />;
const b = <B key="b" />;
const c = <C key="c" />;
const d = <D key="d" />;
const e = <E key="e" />;
function Parent({ children }) {
return children;
}
// 1. Render a normal version of [a, b, c, d, e].
let container = document.createElement('div');
act(() => ReactDOM.render(<Parent>{[a, b, c, d, e]}</Parent>, container));
expect(store).toMatchSnapshot('1: abcde');
expect(container.textContent).toMatch('abcde');
const snapshotForABCDE = print(store);
// 2. Render a version where <C /> renders an <X /> child instead of 'c'.
// This is how we'll test an update to a single component.
act(() => {
setShowX(true);
});
expect(store).toMatchSnapshot('2: abxde');
expect(container.textContent).toMatch('abxde');
const snapshotForABXDE = print(store);
// 3. Verify flipping it back produces the original result.
act(() => {
setShowX(false);
});
expect(container.textContent).toMatch('abcde');
expect(print(store)).toBe(snapshotForABCDE);
// 4. Clean up.
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
// Now comes the interesting part.
// All of these cases are equivalent to [a, b, c, d, e] in output.
// We'll verify that DevTools produces the same snapshots for them.
// These cases are picked so that rendering them sequentially in the same
// container results in a combination of mounts, updates, unmounts, and reorders.
// prettier-ignore
let cases = [
[a, b, c, d, e],
[[a], b, c, d, e],
[[a, b], c, d, e],
[[a, b], c, [d, e]],
[[a, b], c, [d, '', e]],
[[a], b, c, d, [e]],
[a, b, [[c]], d, e],
[[a, ''], [b], [c], [d], [e]],
[a, b, [c, [d, ['', e]]]],
[a, b, c, d, e],
[<div key="0">{a}</div>, b, c, d, e],
[<div key="0">{a}{b}</div>, c, d, e],
[<div key="0">{a}{b}</div>, c, <div key="1">{d}{e}</div>],
[<div key="1">{a}{b}</div>, c, <div key="0">{d}{e}</div>],
[<div key="0">{a}{b}</div>, c, <div key="1">{d}{e}</div>],
[<div key="2">{a}{b}</div>, c, <div key="3">{d}{e}</div>],
[<span key="0">{a}</span>, b, c, d, [e]],
[a, b, <span key="0"><span>{c}</span></span>, d, e],
[<div key="0">{a}</div>, [b], <span key="1">{c}</span>, [d], <div key="2">{e}</div>],
[a, b, [c, <div key="0">{d}<span>{e}</span></div>], ''],
[a, [[]], b, c, [d, [[]], e]],
[[[a, b, c, d], e]],
[a, b, c, d, e]
];
// 5. Test fresh mount for each case.
for (let i = 0; i < cases.length; i++) {
// Ensure fresh mount.
container = document.createElement('div');
// Verify mounting 'abcde'.
act(() => ReactDOM.render(<Parent>{cases[i]}</Parent>, container));
expect(container.textContent).toMatch('abcde');
expect(print(store)).toEqual(snapshotForABCDE);
// Verify switching to 'abxde'.
act(() => {
setShowX(true);
});
expect(container.textContent).toMatch('abxde');
expect(print(store)).toBe(snapshotForABXDE);
// Verify switching back to 'abcde'.
act(() => {
setShowX(false);
});
expect(container.textContent).toMatch('abcde');
expect(print(store)).toBe(snapshotForABCDE);
// Clean up.
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
}
// 6. Verify *updates* by reusing the container between iterations.
// There'll be no unmounting until the very end.
container = document.createElement('div');
for (let i = 0; i < cases.length; i++) {
// Verify mounting 'abcde'.
act(() => ReactDOM.render(<Parent>{cases[i]}</Parent>, container));
expect(container.textContent).toMatch('abcde');
expect(print(store)).toEqual(snapshotForABCDE);
// Verify switching to 'abxde'.
act(() => {
setShowX(true);
});
expect(container.textContent).toMatch('abxde');
expect(print(store)).toBe(snapshotForABXDE);
// Verify switching back to 'abcde'.
act(() => {
setShowX(false);
});
expect(container.textContent).toMatch('abcde');
expect(print(store)).toBe(snapshotForABCDE);
// Don't unmount. Reuse the container between iterations.
}
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
// 7. Same as the previous step, but for Concurrent Mode.
container = document.createElement('div');
// $FlowFixMe
let root = ReactDOM.unstable_createRoot(container);
for (let i = 0; i < cases.length; i++) {
// Verify mounting 'abcde'.
act(() => root.render(<Parent>{cases[i]}</Parent>));
expect(container.textContent).toMatch('abcde');
expect(print(store)).toEqual(snapshotForABCDE);
// Verify switching to 'abxde'.
act(() => {
setShowX(true);
});
expect(container.textContent).toMatch('abxde');
expect(print(store)).toBe(snapshotForABXDE);
// Verify switching back to 'abcde'.
act(() => {
setShowX(false);
});
expect(container.textContent).toMatch('abcde');
expect(print(store)).toBe(snapshotForABCDE);
// Don't unmount. Reuse the container between iterations.
}
act(() => root.unmount());
expect(print(store)).toBe('');
});
it('should handle stress test with reordering', () => {
const A = () => 'a';
const B = () => 'b';
const C = () => 'c';
const D = () => 'd';
const E = () => 'e';
const a = <A key="a" />;
const b = <B key="b" />;
const c = <C key="c" />;
const d = <D key="d" />;
const e = <E key="e" />;
// prettier-ignore
let steps = [
a,
b,
c,
d,
e,
[a],
[b],
[c],
[d],
[e],
[a, b],
[b, a],
[b, c],
[c, b],
[a, c],
[c, a],
];
const Root = ({ children }) => {
return children;
};
// 1. Capture the expected render result.
let snapshots = [];
let container = document.createElement('div');
for (let i = 0; i < steps.length; i++) {
act(() => ReactDOM.render(<Root>{steps[i]}</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 that we can update from every step to every other step and back.
for (let i = 0; i < steps.length; i++) {
for (let j = 0; j < steps.length; j++) {
let container = document.createElement('div');
act(() => ReactDOM.render(<Root>{steps[i]}</Root>, container));
expect(print(store)).toMatch(snapshots[i]);
act(() => ReactDOM.render(<Root>{steps[j]}</Root>, container));
expect(print(store)).toMatch(snapshots[j]);
act(() => ReactDOM.render(<Root>{steps[i]}</Root>, container));
expect(print(store)).toMatch(snapshots[i]);
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
}
}
// 3. Same test as above, but this time we wrap children in a host component.
for (let i = 0; i < steps.length; i++) {
for (let j = 0; j < steps.length; j++) {
let container = document.createElement('div');
act(() =>
ReactDOM.render(
<Root>
<div>{steps[i]}</div>
</Root>,
container
)
);
expect(print(store)).toMatch(snapshots[i]);
act(() =>
ReactDOM.render(
<Root>
<div>{steps[j]}</div>
</Root>,
container
)
);
expect(print(store)).toMatch(snapshots[j]);
act(() =>
ReactDOM.render(
<Root>
<div>{steps[i]}</div>
</Root>,
container
)
);
expect(print(store)).toMatch(snapshots[i]);
act(() => ReactDOM.unmountComponentAtNode(container));
expect(print(store)).toBe('');
}
}
});
it('should handle a stress test for Suspense', async () => {
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 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}>{steps[i]}</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.
for (let i = 0; i < steps.length; i++) {
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={steps[i]}>
<Z />
<Never />
<Z />
</React.Suspense>
<Y />
</Root>,
container
)
);
expect(print(store)).toEqual(snapshots[i]);
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}>{steps[i]}</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}>{steps[j]}</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}>{steps[i]}</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 />
<Never />
<Z />
</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]}>
<Z />
<Never />
<Z />
</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]}>
<Z />
<Never />
<Z />
</React.Suspense>
<Y />
</Root>,
container
)
);
expect(print(store)).toEqual(snapshots[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}>{steps[i]}</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]}>
<Z />
<Never />
<Z />
</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}>{steps[i]}</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]}>
<Z />
<Never />
<Z />
</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}>{steps[j]}</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]}>
<Z />
<Never />
<Z />
</React.Suspense>
<Y />
</Root>,
container
)
);
expect(print(store)).toEqual(snapshots[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]}>{steps[i]}</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(snapshots[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]}>
<Z />
<Never />
<Z />
</React.Suspense>
<Y />
</Root>,
container
)
);
expect(print(store)).toEqual(snapshots[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(snapshots[j]);
// Switch to primary mode.
act(() =>
ReactDOM.render(
<Root>
<X />
<React.Suspense fallback={steps[j]}>{steps[i]}</React.Suspense>
<Y />
</Root>,
container
)
);
// Fallback is still forced though.
expect(print(store)).toEqual(snapshots[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('');
}
}
// TODO: Test Concurrent Mode
});
});
+167 -149
View File
@@ -828,69 +828,62 @@ export function attach(
}
}
function maybeRecordUpdate(fiber: Fiber, hasChildOrderChanged: boolean) {
if (__DEBUG__) {
debug('maybeRecordUpdate()', fiber);
}
function recordTreeDuration(fiber: Fiber) {
const id = getFiberID(getPrimaryFiber(fiber));
const { actualDuration, treeBaseDuration } = fiber;
const isProfilingSupported = fiber.hasOwnProperty('treeBaseDuration');
if (isProfilingSupported) {
const id = getFiberID(getPrimaryFiber(fiber));
const { actualDuration, treeBaseDuration } = fiber;
idToTreeBaseDurationMap.set(id, fiber.treeBaseDuration);
idToTreeBaseDurationMap.set(id, fiber.treeBaseDuration);
if (isProfiling) {
if (treeBaseDuration !== fiber.alternate.treeBaseDuration) {
// Tree base duration updates are included in the operations typed array.
// So we have to convert them from milliseconds to microseconds so we can send them as ints.
const treeBaseDuration = Math.floor(fiber.treeBaseDuration * 1000);
if (isProfiling) {
if (treeBaseDuration !== fiber.alternate.treeBaseDuration) {
// Tree base duration updates are included in the operations typed array.
// So we have to convert them from milliseconds to microseconds so we can send them as ints.
const treeBaseDuration = Math.floor(fiber.treeBaseDuration * 1000);
beginNextOperation(3);
nextOperation[0] = TREE_OPERATION_UPDATE_TREE_BASE_DURATION;
nextOperation[1] = getFiberID(getPrimaryFiber(fiber));
nextOperation[2] = treeBaseDuration;
endNextOperation(false);
}
beginNextOperation(3);
nextOperation[0] = TREE_OPERATION_UPDATE_TREE_BASE_DURATION;
nextOperation[1] = getFiberID(getPrimaryFiber(fiber));
nextOperation[2] = treeBaseDuration;
endNextOperation(false);
}
if (haveProfilerTimesChanged(fiber.alternate, fiber)) {
if (actualDuration > 0) {
// If profiling is active, store durations for elements that were rendered during the commit.
const metadata = ((currentCommitProfilingMetadata: any): CommitProfilingData);
metadata.actualDurations.push(id, actualDuration);
metadata.maxActualDuration = Math.max(
metadata.maxActualDuration,
actualDuration
);
}
if (haveProfilerTimesChanged(fiber.alternate, fiber)) {
if (actualDuration > 0) {
// If profiling is active, store durations for elements that were rendered during the commit.
const metadata = ((currentCommitProfilingMetadata: any): CommitProfilingData);
metadata.actualDurations.push(id, actualDuration);
metadata.maxActualDuration = Math.max(
metadata.maxActualDuration,
actualDuration
);
}
}
}
}
function recordResetChildren(fiber: Fiber, childSet: Fiber) {
// The frontend only really cares about the displayName, key, and children.
// The first two don't really change, so we are only concerned with the order of children here.
// This is trickier than a simple comparison though, since certain types of fibers are filtered.
if (hasChildOrderChanged) {
const nextChildren: Array<number> = [];
const nextChildren: Array<number> = [];
// This is a naive implimentation that shallowly recurses children.
// We might want to revisit this if it proves to be too inefficient.
let child = fiber.child;
while (child !== null) {
findReorderedChildrenRecursively(child, nextChildren);
child = child.sibling;
}
const numChildren = nextChildren.length;
beginNextOperation(3 + numChildren);
nextOperation[0] = TREE_OPERATION_RESET_CHILDREN;
nextOperation[1] = getFiberID(getPrimaryFiber(fiber));
nextOperation[2] = numChildren;
for (let i = 0; i < nextChildren.length; i++) {
nextOperation[3 + i] = nextChildren[i];
}
endNextOperation(false);
// This is a naive implimentation that shallowly recurses children.
// We might want to revisit this if it proves to be too inefficient.
let child = childSet;
while (child !== null) {
findReorderedChildrenRecursively(child, nextChildren);
child = child.sibling;
}
const numChildren = nextChildren.length;
beginNextOperation(3 + numChildren);
nextOperation[0] = TREE_OPERATION_RESET_CHILDREN;
nextOperation[1] = getFiberID(getPrimaryFiber(fiber));
nextOperation[2] = numChildren;
for (let i = 0; i < nextChildren.length; i++) {
nextOperation[3 + i] = nextChildren[i];
}
endNextOperation(false);
}
function findReorderedChildrenRecursively(
@@ -908,15 +901,18 @@ export function attach(
}
}
// Returns whether closest unfiltered fiber parent needs to reset its child list.
function updateFiberRecursively(
nextFiber: Fiber,
prevFiber: Fiber,
parentFiber: Fiber | null
) {
): boolean {
if (__DEBUG__) {
debug('updateFiberRecursively()', nextFiber, parentFiber);
}
const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
const isSuspense = nextFiber.tag === SuspenseComponent;
let shouldResetChildren = false;
// The behavior of timed-out Suspense trees is unique.
// Rather than unmount the timed out content (and possibly lose important state),
// React re-parents this content within a hidden Fragment while the fallback is showing.
@@ -924,108 +920,130 @@ export function attach(
// It might even result in a bad user experience for e.g. node selection in the Elements panel.
// The easiest fix is to strip out the intermediate Fragment fibers,
// so the Elements panel and Profiler don't need to special case them.
if (nextFiber.tag === SuspenseComponent) {
// Suspense components only have a non-null memoizedState if they're timed-out.
const prevDidTimeout = prevFiber.memoizedState !== null;
const nextDidTimeOut = nextFiber.memoizedState !== null;
// The logic below is inspired by the codepaths in updateSuspenseComponent()
// inside ReactFiberBeginWork in the React source code.
if (prevDidTimeout) {
if (nextDidTimeOut) {
// Fallback -> Fallback:
// 1. Reconcile fallback set.
const nextFallbackChildSet = nextFiber.child.sibling;
// Note: We can't use nextFiber.child.sibling.alternate
// because the set is special and alternate may not exist.
const prevFallbackChildSet = prevFiber.child.sibling;
updateFiberRecursively(
nextFallbackChildSet,
prevFallbackChildSet,
nextFiber
);
return;
} else {
// Fallback -> Primary:
// 1. Unmount fallback set
// Note: don't emulate fallback unmount because React actually did it.
// 2. Mount primary set
const nextPrimaryChildSet = nextFiber.child;
mountFiberRecursively(nextPrimaryChildSet, nextFiber, true);
return;
// Suspense components only have a non-null memoizedState if they're timed-out.
const prevDidTimeout = isSuspense && prevFiber.memoizedState !== null;
const nextDidTimeOut = isSuspense && nextFiber.memoizedState !== null;
// The logic below is inspired by the codepaths in updateSuspenseComponent()
// inside ReactFiberBeginWork in the React source code.
if (prevDidTimeout && nextDidTimeOut) {
// Fallback -> Fallback:
// 1. Reconcile fallback set.
const nextFallbackChildSet = nextFiber.child.sibling;
// Note: We can't use nextFiber.child.sibling.alternate
// because the set is special and alternate may not exist.
const prevFallbackChildSet = prevFiber.child.sibling;
if (
updateFiberRecursively(
nextFallbackChildSet,
prevFallbackChildSet,
nextFiber
)
) {
shouldResetChildren = true;
}
} else if (prevDidTimeout && !nextDidTimeOut) {
// Fallback -> Primary:
// 1. Unmount fallback set
// Note: don't emulate fallback unmount because React actually did it.
// 2. Mount primary set
const nextPrimaryChildSet = nextFiber.child;
if (nextPrimaryChildSet !== null) {
mountFiberRecursively(nextPrimaryChildSet, nextFiber, true);
}
shouldResetChildren = true;
} else if (!prevDidTimeout && nextDidTimeOut) {
// Primary -> Fallback:
// 1. Hide primary set
// This is not a real unmount, so it won't get reported by React.
// By this point it's *too late* to find the previous primary child set
// so we'll just tell the store to "forget" about those children.
// They might "resurface" later when we switch to primary content,
// but from the store's point of view they will be a new tree.
recordRecursiveRemoveChildren(nextFiber);
// 2. Mount fallback set
const nextFallbackChildSet = nextFiber.child.sibling;
mountFiberRecursively(nextFallbackChildSet, nextFiber, true);
shouldResetChildren = true;
} else {
// Common case: Primary -> Primary.
// This is the same codepath as for non-Suspense fibers.
if (nextFiber.child !== prevFiber.child) {
// If the first child is different, we need to traverse them.
// Each next child will be either a new child (mount) or an alternate (update).
let nextChild = nextFiber.child;
let prevChildAtSameIndex = prevFiber.child;
while (nextChild) {
// We already know children will be referentially different because
// they are either new mounts or alternates of previous children.
// Schedule updates and mounts depending on whether alternates exist.
// We don't track deletions here because they are reported separately.
if (nextChild.alternate) {
const prevChild = nextChild.alternate;
if (
updateFiberRecursively(
nextChild,
prevChild,
shouldIncludeInTree ? nextFiber : parentFiber
)
) {
// If a nested tree child order changed but it can't handle its own
// child order invalidation (e.g. because it's filtered out like host nodes),
// propagate the need to reset child order upwards to this Fiber.
shouldResetChildren = true;
}
// However we also keep track if the order of the children matches
// the previous order. They are always different referentially, but
// if the instances line up conceptually we'll want to know that.
if (prevChild !== prevChildAtSameIndex) {
shouldResetChildren = true;
}
} else {
mountFiberRecursively(
nextChild,
shouldIncludeInTree ? nextFiber : parentFiber
);
shouldResetChildren = true;
}
// Try the next child.
nextChild = nextChild.sibling;
// Advance the pointer in the previous list so that we can
// keep comparing if they line up.
if (!shouldResetChildren && prevChildAtSameIndex !== null) {
prevChildAtSameIndex = prevChildAtSameIndex.sibling;
}
}
} else {
if (nextDidTimeOut) {
// Primary -> Fallback:
// 1. Hide primary set
// This is not a real unmount, so it won't get reported by React.
// By this point it's *too late* to find the previous primary child set
// so we'll just tell the store to "forget" about those children.
// They might "resurface" later when we switch to primary content,
// but from the store's point of view they will be a new tree.
recordRecursiveRemoveChildren(nextFiber);
// 2. Mount fallback set
const nextFallbackChildSet = nextFiber.child.sibling;
mountFiberRecursively(nextFallbackChildSet, nextFiber, true);
return;
} else {
// Primary -> Primary:
// 1. Reconcile primary set.
// Note: no return so we can passthrough to the logic below.
// If we have no more children, but used to, they don't line up.
if (prevChildAtSameIndex !== null) {
shouldResetChildren = true;
}
}
}
const shouldIncludeInTree = !shouldFilterFiber(nextFiber);
let hasChildOrderChanged = false;
if (nextFiber.child !== prevFiber.child) {
// If the first child is different, we need to traverse them.
// Each next child will be either a new child (mount) or an alternate (update).
let nextChild = nextFiber.child;
let prevChildAtSameIndex = prevFiber.child;
while (nextChild) {
// We already know children will be referentially different because
// they are either new mounts or alternates of previous children.
// Schedule updates and mounts depending on whether alternates exist.
// We don't track deletions here because they are reported separately.
if (nextChild.alternate) {
const prevChild = nextChild.alternate;
updateFiberRecursively(
nextChild,
prevChild,
shouldIncludeInTree ? nextFiber : parentFiber
);
// However we also keep track if the order of the children matches
// the previous order. They are always different referentially, but
// if the instances line up conceptually we'll want to know that.
if (!hasChildOrderChanged && prevChild !== prevChildAtSameIndex) {
hasChildOrderChanged = true;
}
} else {
mountFiberRecursively(
nextChild,
shouldIncludeInTree ? nextFiber : parentFiber
);
if (!hasChildOrderChanged) {
hasChildOrderChanged = true;
}
}
// Try the next child.
nextChild = nextChild.sibling;
// Advance the pointer in the previous list so that we can
// keep comparing if they line up.
if (!hasChildOrderChanged && prevChildAtSameIndex != null) {
prevChildAtSameIndex = prevChildAtSameIndex.sibling;
}
}
// If we have no more children, but used to, they don't line up.
if (!hasChildOrderChanged && prevChildAtSameIndex != null) {
hasChildOrderChanged = true;
}
}
if (shouldIncludeInTree) {
maybeRecordUpdate(nextFiber, hasChildOrderChanged);
const isProfilingSupported = nextFiber.hasOwnProperty('treeBaseDuration');
if (isProfilingSupported) {
recordTreeDuration(nextFiber);
}
}
if (shouldResetChildren) {
// We need to crawl the subtree for closest non-filtered Fibers
// so that we can display them in a flat children set.
if (shouldIncludeInTree) {
// Normally, search for children from the rendered child.
let nextChildSet = nextFiber.child;
if (nextDidTimeOut) {
// Special case: timed-out Suspense renders the fallback set.
nextChildSet = nextFiber.child.sibling;
}
recordResetChildren(nextFiber, nextChildSet);
// We've handled the child order change for this Fiber.
// Since it's included, there's no need to invalidate parent child order.
return false;
} else {
// Let the closest unfiltered parent Fiber reset its child order instead.
return true;
}
} else {
return false;
}
}
+23 -15
View File
@@ -9180,15 +9180,20 @@ react-color@^2.11.7:
reactcss "^1.2.0"
tinycolor2 "^1.4.1"
react-dom@^16.9.0-alpha.0:
version "16.9.0-alpha.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.9.0-alpha.0.tgz#9dfaec18ac1a500fa72cab7b70f2ae29d0cd7716"
integrity sha512-BQ5gN42yIPuTnBvE6K9vSjNfDRpSNcYCs2sUx9XR5VaWKwlHTt3G6qIWK6zdXy8TYKb1+IxpsAI0RtbRdXQZ2A==
react-dom@0.0.0-4221565e1:
version "0.0.0-4221565e1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-4221565e1.tgz#d9044312a3294a8828867624a9fea7fd9aa76570"
integrity sha512-uoBFBMDt5OOAZQNWl72hqCuvVxE1YpAILqIHSwv2ktiLcHPmyByftiuFdqrjXVCJe3LR9sAQWrB8xWgC2bCi+w==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.14.0-alpha.0"
scheduler "0.0.0-4221565e1"
react-is@0.0.0-4221565e1:
version "0.0.0-4221565e1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-0.0.0-4221565e1.tgz#b9516ac872939c5d805f970e7aff4b9d3b3cc99e"
integrity sha512-Ko7j3yyyQi5aB8tw3TmpmHrjeZbrzKm2FCKNpXJyXFAeummKLzeWH+M/gCh9w6EA6sZcX2W/YAX4U/KmeWjsWw==
react-is@^16.8.1:
version "16.8.3"
@@ -9200,11 +9205,6 @@ react-is@^16.8.4:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.4.tgz#90f336a68c3a29a096a3d648ab80e87ec61482a2"
integrity sha512-PVadd+WaUDOAciICm/J1waJaSvgq+4rHE/K70j0PFqKhkTBsPv/82UGQJNXAngz1fOQLLxI6z1sEDmJDQhCTAA==
react-is@^16.9.0-alpha.0:
version "16.9.0-alpha.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0-alpha.0.tgz#af943642da49460f9fe98630182b0a62db3e9e6a"
integrity sha512-psl0ePLTFliYfwcbwvimLgTNN156ZdeWB4zvP7dV/6lTAqWMHFfidg/mSZ2fFgE1LMNN8ZJOLl2DfZ8yg+3ETA==
react-lifecycles-compat@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
@@ -9223,15 +9223,15 @@ react-window@^1.8.0:
"@babel/runtime" "^7.0.0"
memoize-one ">=3.1.1 <6"
react@^16.9.0-alpha.0:
version "16.9.0-alpha.0"
resolved "https://registry.yarnpkg.com/react/-/react-16.9.0-alpha.0.tgz#e350f3d8af36e3251079cbc90d304620e2f78ccb"
integrity sha512-y4bu7rJvtnPPsIwOj7sp5Y2SqlOb0jFupfkdjWxxn8ZeqzUARgpR9wJBUVwW1/QosVdOblmApjo/j6iiAXnebA==
react@0.0.0-4221565e1:
version "0.0.0-4221565e1"
resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-4221565e1.tgz#bc7f3e67f2c2c53693cda2e89e6fa71bfc89ebcd"
integrity sha512-iVAljelOxza6S9QcFMkxJCcj2Y+QDiv6XwGz9A8c5jtKejGOEKK/Xumg7uTDmGwy8vjGVDI7qGDW8Rta80xH9g==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
scheduler "^0.14.0-alpha.0"
scheduler "0.0.0-4221565e1"
reactcss@^1.2.0:
version "1.2.3"
@@ -9882,6 +9882,14 @@ sax@>=0.6.0, sax@^1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
scheduler@0.0.0-4221565e1:
version "0.0.0-4221565e1"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-4221565e1.tgz#70c99271bd7bc4fbf4e7c69f31c003c4c61cd285"
integrity sha512-RhQ6QhkRJDXAcs6woR6p9UYDyjRguRsYXpWDFbCft45lWs4sSfszxPRr5TJVVLjIwPu0Wy0Mkd75smQ8TMponA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler@^0.14.0-alpha.0:
version "0.14.0-alpha.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.14.0-alpha.0.tgz#6d301d8fd10373487e0e47e837ad24e863ce807d"