diff --git a/package.json b/package.json index 54cbba4e12..60850a905b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/__tests__/__snapshots__/store-test.js.snap b/src/__tests__/__snapshots__/store-test.js.snap index cf299a8c5e..102a328558 100644 --- a/src/__tests__/__snapshots__/store-test.js.snap +++ b/src/__tests__/__snapshots__/store-test.js.snap @@ -25,142 +25,6 @@ exports[`Store should filter DOM nodes from the store tree: 1: mount 1`] = ` `; -exports[`Store should handle a stress test for Suspense 1`] = ` -[root] - ▾ - - ▾ - - -`; - -exports[`Store should handle a stress test for Suspense 2`] = ` -[root] - ▾ - - ▾ - - -`; - -exports[`Store should handle a stress test for Suspense 3`] = ` -[root] - ▾ - - ▾ - - - - -`; - -exports[`Store should handle a stress test for Suspense 4`] = ` -[root] - ▾ - - ▾ - - - - -`; - -exports[`Store should handle a stress test for Suspense 5`] = ` -[root] - ▾ - - ▾ - - - -`; - -exports[`Store should handle a stress test for Suspense 6`] = ` -[root] - ▾ - - ▾ - - - -`; - -exports[`Store should handle a stress test for Suspense 7`] = ` -[root] - ▾ - - ▾ - - - -`; - -exports[`Store should handle a stress test for Suspense 8`] = ` -[root] - ▾ - - ▾ - - - -`; - -exports[`Store should handle a stress test for Suspense 9`] = ` -[root] - ▾ - - ▾ - - -`; - -exports[`Store should handle a stress test for Suspense 10`] = ` -[root] - ▾ - - - -`; - -exports[`Store should handle a stress test for Suspense 11`] = ` -[root] - ▾ - - ▾ - - -`; - -exports[`Store should handle a stress test for Suspense 12`] = ` -[root] - ▾ - - ▾ - - -`; - -exports[`Store should handle a stress test with different tree operations: 1: abcde 1`] = ` -[root] - ▾ - - - - - -`; - -exports[`Store should handle a stress test with different tree operations: 2: abxde 1`] = ` -[root] - ▾ - - - ▾ - - - -`; - exports[`Store should support collapsing parts of the tree: 1: mount 1`] = ` [root] ▾ diff --git a/src/__tests__/__snapshots__/storeStress-test.js.snap b/src/__tests__/__snapshots__/storeStress-test.js.snap new file mode 100644 index 0000000000..9deb51fb38 --- /dev/null +++ b/src/__tests__/__snapshots__/storeStress-test.js.snap @@ -0,0 +1,239 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StoreStress should handle a stress test for Suspense 1`] = ` +[root] + ▾ + + ▾ + + +`; + +exports[`StoreStress should handle a stress test for Suspense 2`] = ` +[root] + ▾ + + ▾ + + +`; + +exports[`StoreStress should handle a stress test for Suspense 3`] = ` +[root] + ▾ + + ▾ + + + + +`; + +exports[`StoreStress should handle a stress test for Suspense 4`] = ` +[root] + ▾ + + ▾ + + + + +`; + +exports[`StoreStress should handle a stress test for Suspense 5`] = ` +[root] + ▾ + + ▾ + + + +`; + +exports[`StoreStress should handle a stress test for Suspense 6`] = ` +[root] + ▾ + + ▾ + + + +`; + +exports[`StoreStress should handle a stress test for Suspense 7`] = ` +[root] + ▾ + + ▾ + + + +`; + +exports[`StoreStress should handle a stress test for Suspense 8`] = ` +[root] + ▾ + + ▾ + + + +`; + +exports[`StoreStress should handle a stress test for Suspense 9`] = ` +[root] + ▾ + + ▾ + + +`; + +exports[`StoreStress should handle a stress test for Suspense 10`] = ` +[root] + ▾ + + + +`; + +exports[`StoreStress should handle a stress test for Suspense 11`] = ` +[root] + ▾ + + ▾ + + +`; + +exports[`StoreStress should handle a stress test for Suspense 12`] = ` +[root] + ▾ + + ▾ + + +`; + +exports[`StoreStress should handle a stress test with different tree operations: 1: abcde 1`] = ` +[root] + ▾ + + + + + +`; + +exports[`StoreStress should handle a stress test with different tree operations: 2: abxde 1`] = ` +[root] + ▾ + + + ▾ + + + +`; + +exports[`StoreStress should handle stress test with reordering 1`] = ` +[root] + ▾ + +`; + +exports[`StoreStress should handle stress test with reordering 2`] = ` +[root] + ▾ + +`; + +exports[`StoreStress should handle stress test with reordering 3`] = ` +[root] + ▾ + +`; + +exports[`StoreStress should handle stress test with reordering 4`] = ` +[root] + ▾ + +`; + +exports[`StoreStress should handle stress test with reordering 5`] = ` +[root] + ▾ + +`; + +exports[`StoreStress should handle stress test with reordering 6`] = ` +[root] + ▾ + +`; + +exports[`StoreStress should handle stress test with reordering 7`] = ` +[root] + ▾ + +`; + +exports[`StoreStress should handle stress test with reordering 8`] = ` +[root] + ▾ + +`; + +exports[`StoreStress should handle stress test with reordering 9`] = ` +[root] + ▾ + +`; + +exports[`StoreStress should handle stress test with reordering 10`] = ` +[root] + ▾ + +`; + +exports[`StoreStress should handle stress test with reordering 11`] = ` +[root] + ▾ + + +`; + +exports[`StoreStress should handle stress test with reordering 12`] = ` +[root] + ▾ + + +`; + +exports[`StoreStress should handle stress test with reordering 13`] = ` +[root] + ▾ + + +`; + +exports[`StoreStress should handle stress test with reordering 14`] = ` +[root] + ▾ + + +`; + +exports[`StoreStress should handle stress test with reordering 15`] = ` +[root] + ▾ + + +`; + +exports[`StoreStress should handle stress test with reordering 16`] = ` +[root] + ▾ + + +`; diff --git a/src/__tests__/store-test.js b/src/__tests__/store-test.js index 9c64c93adb..82da076f0a 100644 --- a/src/__tests__/store-test.js +++ b/src/__tests__/store-test.js @@ -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 = () =>
Loading...
; const SuspendingComponent = () => { throw new Promise(() => {}); @@ -124,12 +118,10 @@ describe('Store', () => { act(() => ReactDOM.render(, container)); expect(store).toMatchSnapshot('1: loading'); - act(async () => { + act(() => { ReactDOM.render(, 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 ? : 'c'; - }; - const D = () => 'd'; - const E = () => 'e'; - const X = () => 'x'; - const a =
; - const b = ; - const c = ; - const d = ; - const 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({[a, b, c, d, e]}, container)); - expect(store).toMatchSnapshot('1: abcde'); - expect(container.textContent).toMatch('abcde'); - const snapshotForABCDE = print(store); - - // 2. Render a version where renders an 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], - [
{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}
], ''], - [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({cases[i]}, 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({cases[i]}, 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({cases[i]})); - 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 =
; - const b = ; - const c = ; - // const x = ; - // const y = ; - const z = ; - - // prettier-ignore - const steps = [ - a, - [a], - [a, b, c], - [c, b, a], - [c, null, a], - {c}{a}, -
{c}{a}
, -
{a}{b}
, - [[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( - - - {steps[i]} - - , - 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( - - - - - - - - - , - 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( - - - {steps[i]} - - , - container - ) - ); - expect(print(store)).toEqual(snapshots[i]); - // Re-render with steps[j]. - act(() => - ReactDOM.render( - - - {steps[j]} - - , - 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( - // - // - // - // - // - // - // - // - // , - // container - // )); - // expect(print(store)).toEqual(snapshots[i]); - // // Re-render with steps[j]. - // act(() => ReactDOM.render( - // - // - // - // - // - // - // - // - // , - // 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( - - - {steps[i]} - - , - container - ) - ); - expect(print(store)).toEqual(snapshots[i]); - // Re-render with steps[j]. - act(() => - ReactDOM.render( - - - - - - - - - , - 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( - // - // - // - // - // - // - // - // - // , - // container - // )); - // expect(print(store)).toEqual(snapshots[i]); - // // Re-render with steps[j]. - // act(() => ReactDOM.render( - // - // - // - // {steps[j]} - // - // - // , - // 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( - // - // - // - // {steps[i]} - // - // - // , - // 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( - // - // - // - // - // - // - // - // - // , - // 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( - // - // - // - // {steps[i]} - // - // - // , - // 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 - }); }); diff --git a/src/__tests__/storeStress-test.js b/src/__tests__/storeStress-test.js new file mode 100644 index 0000000000..ae13007cca --- /dev/null +++ b/src/__tests__/storeStress-test.js @@ -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 ? : 'c'; + }; + const D = () => 'd'; + const E = () => 'e'; + const X = () => 'x'; + const a =
; + const b = ; + const c = ; + const d = ; + const 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({[a, b, c, d, e]}, container)); + expect(store).toMatchSnapshot('1: abcde'); + expect(container.textContent).toMatch('abcde'); + const snapshotForABCDE = print(store); + + // 2. Render a version where renders an 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], + [
{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}
], ''], + [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({cases[i]}, 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({cases[i]}, 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({cases[i]})); + 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 =
; + const b = ; + const c = ; + const d = ; + const 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({steps[i]}, 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({steps[i]}, container)); + expect(print(store)).toMatch(snapshots[i]); + act(() => ReactDOM.render({steps[j]}, container)); + expect(print(store)).toMatch(snapshots[j]); + act(() => ReactDOM.render({steps[i]}, 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( + +
{steps[i]}
+
, + container + ) + ); + expect(print(store)).toMatch(snapshots[i]); + act(() => + ReactDOM.render( + +
{steps[j]}
+
, + container + ) + ); + expect(print(store)).toMatch(snapshots[j]); + act(() => + ReactDOM.render( + +
{steps[i]}
+
, + 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 =
; + const b = ; + const c = ; + const z = ; + + // prettier-ignore + const steps = [ + a, + [a], + [a, b, c], + [c, b, a], + [c, null, a], + {c}{a}, +
{c}{a}
, +
{a}{b}
, + [[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( + + + {steps[i]} + + , + 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( + + + + + + + + + , + 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( + + + {steps[i]} + + , + container + ) + ); + expect(print(store)).toEqual(snapshots[i]); + // Re-render with steps[j]. + act(() => + ReactDOM.render( + + + {steps[j]} + + , + container + ) + ); + // Verify the successful transition to steps[j]. + expect(print(store)).toEqual(snapshots[j]); + // Check that we can transition back again. + act(() => + ReactDOM.render( + + + {steps[i]} + + , + 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( + + + + + + + + + , + container + ) + ); + expect(print(store)).toEqual(snapshots[i]); + // Re-render with steps[j]. + act(() => + ReactDOM.render( + + + + + + + + + , + container + ) + ); + // Verify the successful transition to steps[j]. + expect(print(store)).toEqual(snapshots[j]); + // Check that we can transition back again. + act(() => + ReactDOM.render( + + + + + + + + + , + 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( + + + {steps[i]} + + , + container + ) + ); + expect(print(store)).toEqual(snapshots[i]); + // Re-render with steps[j]. + act(() => + ReactDOM.render( + + + + + + + + + , + container + ) + ); + // Verify the successful transition to steps[j]. + expect(print(store)).toEqual(snapshots[j]); + // Check that we can transition back again. + act(() => + ReactDOM.render( + + + {steps[i]} + + , + 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( + + + + + + + + + , + container + ) + ); + expect(print(store)).toEqual(snapshots[i]); + // Re-render with steps[j]. + act(() => + ReactDOM.render( + + + {steps[j]} + + , + container + ) + ); + // Verify the successful transition to steps[j]. + expect(print(store)).toEqual(snapshots[j]); + // Check that we can transition back again. + act(() => + ReactDOM.render( + + + + + + + + + , + 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( + + + {steps[i]} + + , + 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( + + + + + + + + + , + 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( + + + {steps[i]} + + , + 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 + }); +}); diff --git a/src/backend/renderer.js b/src/backend/renderer.js index 7ce924448f..7aa8b1555a 100644 --- a/src/backend/renderer.js +++ b/src/backend/renderer.js @@ -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 = []; + const nextChildren: Array = []; - // 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; } } diff --git a/yarn.lock b/yarn.lock index 63269e7bad..dfd34ac54d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"