From ccf27589b9922a177131d1fc00c2c1912d520546 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 14 Oct 2019 15:17:50 -0700 Subject: [PATCH] Remove concurrent APIs from stable builds Those who want to try concurrent mode should use the experimental builds instead. I've left the `unstable_` prefixed APIs in the Facebook build so we can continue experimenting with them internally without blessing them for widespread use. --- fixtures/dom/src/__tests__/wrong-act-test.js | 35 +- .../storeStressTestConcurrent-test.js.snap | 493 --------- .../storeStressTestConcurrent-test.js | 323 +++++- .../ReactDOMFiberAsync-test.internal.js | 943 +++++++++--------- .../src/__tests__/ReactDOMHooks-test.js | 64 +- .../src/__tests__/ReactDOMRoot-test.js | 29 +- ...DOMServerPartialHydration-test.internal.js | 70 +- ...MServerSelectiveHydration-test.internal.js | 20 +- .../ReactDOMServerSuspense-test.internal.js | 11 +- .../__tests__/ReactServerRendering-test.js | 56 +- .../ReactServerRenderingHydration-test.js | 175 +--- .../src/__tests__/ReactTestUtilsAct-test.js | 124 +-- .../ReactUnmockedSchedulerWarning-test.js | 56 +- .../src/__tests__/ReactUpdates-test.js | 126 +-- packages/react-dom/src/client/ReactDOM.js | 37 +- packages/react-dom/src/client/ReactDOMFB.js | 28 +- .../ChangeEventPlugin-test.internal.js | 562 +++++------ .../DOMEventResponderSystem-test.internal.js | 84 +- .../SimpleEventPlugin-test.internal.js | 450 +++++---- .../src/dom/__tests__/Input-test.internal.js | 344 +++---- .../MixedResponders-test-internal.js | 13 +- .../src/__tests__/ReactFresh-test.js | 4 +- .../ReactDOMTracing-test.internal.js | 24 +- .../ReactProfilerDOM-test.internal.js | 7 +- packages/shared/ReactFeatureFlags.js | 2 +- .../forks/ReactFeatureFlags.native-fb.js | 2 +- .../forks/ReactFeatureFlags.native-oss.js | 2 +- .../forks/ReactFeatureFlags.persistent.js | 2 +- .../forks/ReactFeatureFlags.test-renderer.js | 2 +- .../ReactFeatureFlags.test-renderer.www.js | 2 +- .../shared/forks/ReactFeatureFlags.www.js | 2 +- 31 files changed, 1903 insertions(+), 2189 deletions(-) delete mode 100644 packages/react-devtools-shared/src/__tests__/__snapshots__/storeStressTestConcurrent-test.js.snap diff --git a/fixtures/dom/src/__tests__/wrong-act-test.js b/fixtures/dom/src/__tests__/wrong-act-test.js index 05dda494d1126..6c054efec1bf9 100644 --- a/fixtures/dom/src/__tests__/wrong-act-test.js +++ b/fixtures/dom/src/__tests__/wrong-act-test.js @@ -17,6 +17,7 @@ let TestRenderer; let ARTTest; global.__DEV__ = process.env.NODE_ENV !== 'production'; +global.__EXPERIMENTAL__ = process.env.RELEASE_CHANNEL === 'experimental'; expect.extend(require('../toWarnDev')); @@ -176,19 +177,21 @@ it("doesn't warn if you use nested acts from different renderers", () => { }); }); -it('warns when using createRoot() + .render', () => { - const root = ReactDOM.unstable_createRoot(document.createElement('div')); - expect(() => { - TestRenderer.act(() => { - root.render(); - }); - }).toWarnDev( - [ - 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked', - "It looks like you're using the wrong act()", - ], - { - withoutStack: true, - } - ); -}); +if (__EXPERIMENTAL__) { + it('warns when using createRoot() + .render', () => { + const root = ReactDOM.createRoot(document.createElement('div')); + expect(() => { + TestRenderer.act(() => { + root.render(); + }); + }).toWarnDev( + [ + 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked', + "It looks like you're using the wrong act()", + ], + { + withoutStack: true, + } + ); + }); +} diff --git a/packages/react-devtools-shared/src/__tests__/__snapshots__/storeStressTestConcurrent-test.js.snap b/packages/react-devtools-shared/src/__tests__/__snapshots__/storeStressTestConcurrent-test.js.snap deleted file mode 100644 index 9527d9f6025de..0000000000000 --- a/packages/react-devtools-shared/src/__tests__/__snapshots__/storeStressTestConcurrent-test.js.snap +++ /dev/null @@ -1,493 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 1`] = ` -[root] - ▾ - - ▾ - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 2`] = ` -[root] - ▾ - - ▾ - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 3`] = ` -[root] - ▾ - - ▾ - - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 4`] = ` -[root] - ▾ - - ▾ - - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 5`] = ` -[root] - ▾ - - ▾ - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 6`] = ` -[root] - ▾ - - ▾ - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 7`] = ` -[root] - ▾ - - ▾ - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 8`] = ` -[root] - ▾ - - ▾ - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 9`] = ` -[root] - ▾ - - ▾ - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 10`] = ` -[root] - ▾ - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 11`] = ` -[root] - ▾ - - ▾ - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 12`] = ` -[root] - ▾ - - ▾ - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 1`] = ` -[root] - ▾ - - ▾ - ▾ - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 2`] = ` -[root] - ▾ - - ▾ - ▾ - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 3`] = ` -[root] - ▾ - - ▾ - ▾ - - - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 4`] = ` -[root] - ▾ - - ▾ - ▾ - - - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 5`] = ` -[root] - ▾ - - ▾ - ▾ - - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 6`] = ` -[root] - ▾ - - ▾ - ▾ - - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 7`] = ` -[root] - ▾ - - ▾ - ▾ - - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 8`] = ` -[root] - ▾ - - ▾ - ▾ - - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 9`] = ` -[root] - ▾ - - ▾ - ▾ - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 10`] = ` -[root] - ▾ - - ▾ - ▾ - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 11`] = ` -[root] - ▾ - - ▾ - ▾ - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 12`] = ` -[root] - ▾ - - ▾ - ▾ - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 13`] = ` -[root] - ▾ - - ▾ - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 14`] = ` -[root] - ▾ - - ▾ - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 15`] = ` -[root] - ▾ - - ▾ - - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 16`] = ` -[root] - ▾ - - ▾ - - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 17`] = ` -[root] - ▾ - - ▾ - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 18`] = ` -[root] - ▾ - - ▾ - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 19`] = ` -[root] - ▾ - - ▾ - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 20`] = ` -[root] - ▾ - - ▾ - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 21`] = ` -[root] - ▾ - - ▾ - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 22`] = ` -[root] - ▾ - - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 23`] = ` -[root] - ▾ - - ▾ - - -`; - -exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 24`] = ` -[root] - ▾ - - ▾ - - -`; - -exports[`StoreStressConcurrent should handle a stress test with different tree operations (Concurrent Mode): 1: abcde 1`] = ` -[root] - ▾ - - - - - -`; - -exports[`StoreStressConcurrent should handle a stress test with different tree operations (Concurrent Mode): 2: abxde 1`] = ` -[root] - ▾ - - - ▾ - - - -`; - -exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 1`] = ` -[root] - ▾ - -`; - -exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 2`] = ` -[root] - ▾ - -`; - -exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 3`] = ` -[root] - ▾ - -`; - -exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 4`] = ` -[root] - ▾ - -`; - -exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 5`] = ` -[root] - ▾ - -`; - -exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 6`] = ` -[root] - ▾ - -`; - -exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 7`] = ` -[root] - ▾ - -`; - -exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 8`] = ` -[root] - ▾ - -`; - -exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 9`] = ` -[root] - ▾ - -`; - -exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 10`] = ` -[root] - ▾ - -`; - -exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 11`] = ` -[root] - ▾ - - -`; - -exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 12`] = ` -[root] - ▾ - - -`; - -exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 13`] = ` -[root] - ▾ - - -`; - -exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 14`] = ` -[root] - ▾ - - -`; - -exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 15`] = ` -[root] - ▾ - - -`; - -exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 16`] = ` -[root] - ▾ - - -`; diff --git a/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js b/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js index b56c058d4bf09..fc4fe8bc1e7d3 100644 --- a/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js +++ b/packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js @@ -27,6 +27,11 @@ describe('StoreStressConcurrent', () => { print = require('./storeSerializer').print; }); + if (!__EXPERIMENTAL__) { + it("empty test so Jest doesn't complain", () => {}); + return; + } + // 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 (Concurrent Mode)', () => { @@ -57,9 +62,19 @@ describe('StoreStressConcurrent', () => { // 1. Render a normal version of [a, b, c, d, e]. let container = document.createElement('div'); // $FlowFixMe - let root = ReactDOM.unstable_createRoot(container); + let root = ReactDOM.createRoot(container); act(() => root.render({[a, b, c, d, e]})); - expect(store).toMatchSnapshot('1: abcde'); + expect(store).toMatchInlineSnapshot( + ` + [root] + ▾ + + + + + + `, + ); expect(container.textContent).toMatch('abcde'); const snapshotForABCDE = print(store); @@ -68,7 +83,18 @@ describe('StoreStressConcurrent', () => { act(() => { setShowX(true); }); - expect(store).toMatchSnapshot('2: abxde'); + expect(store).toMatchInlineSnapshot( + ` + [root] + ▾ + + + ▾ + + + + `, + ); expect(container.textContent).toMatch('abxde'); const snapshotForABXDE = print(store); @@ -120,7 +146,7 @@ describe('StoreStressConcurrent', () => { // Ensure fresh mount. container = document.createElement('div'); // $FlowFixMe - root = ReactDOM.unstable_createRoot(container); + root = ReactDOM.createRoot(container); // Verify mounting 'abcde'. act(() => root.render({cases[i]})); @@ -150,7 +176,7 @@ describe('StoreStressConcurrent', () => { // There'll be no unmounting until the very end. container = document.createElement('div'); // $FlowFixMe - root = ReactDOM.unstable_createRoot(container); + root = ReactDOM.createRoot(container); for (let i = 0; i < cases.length; i++) { // Verify mounting 'abcde'. act(() => root.render({cases[i]})); @@ -216,22 +242,80 @@ describe('StoreStressConcurrent', () => { let snapshots = []; let container = document.createElement('div'); // $FlowFixMe - let root = ReactDOM.unstable_createRoot(container); + let root = ReactDOM.createRoot(container); for (let i = 0; i < steps.length; i++) { act(() => root.render({steps[i]})); // We snapshot each step once so it doesn't regress. - expect(store).toMatchSnapshot(); snapshots.push(print(store)); act(() => root.unmount()); expect(print(store)).toBe(''); } + expect(snapshots).toMatchInlineSnapshot(` + Array [ + "[root] + ▾ + ", + "[root] + ▾ + ", + "[root] + ▾ + ", + "[root] + ▾ + ", + "[root] + ▾ + ", + "[root] + ▾ + ", + "[root] + ▾ + ", + "[root] + ▾ + ", + "[root] + ▾ + ", + "[root] + ▾ + ", + "[root] + ▾ + + ", + "[root] + ▾ + + ", + "[root] + ▾ + + ", + "[root] + ▾ + + ", + "[root] + ▾ + + ", + "[root] + ▾ + + ", + ] + `); + // 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++) { container = document.createElement('div'); // $FlowFixMe - root = ReactDOM.unstable_createRoot(container); + root = ReactDOM.createRoot(container); act(() => root.render({steps[i]})); expect(print(store)).toMatch(snapshots[i]); act(() => root.render({steps[j]})); @@ -248,7 +332,7 @@ describe('StoreStressConcurrent', () => { for (let j = 0; j < steps.length; j++) { container = document.createElement('div'); // $FlowFixMe - root = ReactDOM.unstable_createRoot(container); + root = ReactDOM.createRoot(container); act(() => root.render( @@ -320,7 +404,7 @@ describe('StoreStressConcurrent', () => { let snapshots = []; let container = document.createElement('div'); // $FlowFixMe - let root = ReactDOM.unstable_createRoot(container); + let root = ReactDOM.createRoot(container); for (let i = 0; i < steps.length; i++) { act(() => root.render( @@ -331,13 +415,96 @@ describe('StoreStressConcurrent', () => { , ), ); - // We snapshot each step once so it doesn't regress. - expect(store).toMatchSnapshot(); + // We snapshot each step once so it doesn't regress.d snapshots.push(print(store)); act(() => root.unmount()); expect(print(store)).toBe(''); } + expect(snapshots).toMatchInlineSnapshot(` + Array [ + "[root] + ▾ + + ▾ + + ", + "[root] + ▾ + + ▾ + + ", + "[root] + ▾ + + ▾ + + + + ", + "[root] + ▾ + + ▾ + + + + ", + "[root] + ▾ + + ▾ + + + ", + "[root] + ▾ + + ▾ + + + ", + "[root] + ▾ + + ▾ + + + ", + "[root] + ▾ + + ▾ + + + ", + "[root] + ▾ + + ▾ + + ", + "[root] + ▾ + + + ", + "[root] + ▾ + + ▾ + + ", + "[root] + ▾ + + ▾ + + ", + ] + `); + // 2. Verify check Suspense can render same steps as initial fallback content. for (let i = 0; i < steps.length; i++) { act(() => @@ -364,7 +531,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); // $FlowFixMe - root = ReactDOM.unstable_createRoot(container); + root = ReactDOM.createRoot(container); act(() => root.render( @@ -410,7 +577,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); // $FlowFixMe - root = ReactDOM.unstable_createRoot(container); + root = ReactDOM.createRoot(container); act(() => root.render( @@ -468,7 +635,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); // $FlowFixMe - root = ReactDOM.unstable_createRoot(container); + root = ReactDOM.createRoot(container); act(() => root.render( @@ -518,7 +685,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); // $FlowFixMe - root = ReactDOM.unstable_createRoot(container); + root = ReactDOM.createRoot(container); act(() => root.render( @@ -572,7 +739,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); // $FlowFixMe - root = ReactDOM.unstable_createRoot(container); + root = ReactDOM.createRoot(container); act(() => root.render( @@ -726,7 +893,7 @@ describe('StoreStressConcurrent', () => { let snapshots = []; let container = document.createElement('div'); // $FlowFixMe - let root = ReactDOM.unstable_createRoot(container); + let root = ReactDOM.createRoot(container); for (let i = 0; i < steps.length; i++) { act(() => root.render( @@ -740,7 +907,6 @@ describe('StoreStressConcurrent', () => { ), ); // We snapshot each step once so it doesn't regress. - expect(store).toMatchSnapshot(); snapshots.push(print(store)); act(() => root.unmount()); expect(print(store)).toBe(''); @@ -765,19 +931,126 @@ describe('StoreStressConcurrent', () => { ), ); // We snapshot each step once so it doesn't regress. - expect(store).toMatchSnapshot(); fallbackSnapshots.push(print(store)); act(() => root.unmount()); expect(print(store)).toBe(''); } + expect(snapshots).toMatchInlineSnapshot(` + Array [ + "[root] + ▾ + + ▾ + ▾ + + + ", + "[root] + ▾ + + ▾ + ▾ + + + ", + "[root] + ▾ + + ▾ + ▾ + + + + + ", + "[root] + ▾ + + ▾ + ▾ + + + + + ", + "[root] + ▾ + + ▾ + ▾ + + + + ", + "[root] + ▾ + + ▾ + ▾ + + + + ", + "[root] + ▾ + + ▾ + ▾ + + + + ", + "[root] + ▾ + + ▾ + ▾ + + + + ", + "[root] + ▾ + + ▾ + ▾ + + + ", + "[root] + ▾ + + ▾ + ▾ + + ", + "[root] + ▾ + + ▾ + ▾ + + + ", + "[root] + ▾ + + ▾ + ▾ + + + ", + ] + `); + // 3. Verify we can update from each step to each step in primary mode. for (let i = 0; i < steps.length; i++) { for (let j = 0; j < steps.length; j++) { // Always start with a fresh container and steps[i]. container = document.createElement('div'); // $FlowFixMe - root = ReactDOM.unstable_createRoot(container); + root = ReactDOM.createRoot(container); act(() => root.render( @@ -829,7 +1102,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); // $FlowFixMe - root = ReactDOM.unstable_createRoot(container); + root = ReactDOM.createRoot(container); act(() => root.render( @@ -896,7 +1169,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); // $FlowFixMe - root = ReactDOM.unstable_createRoot(container); + root = ReactDOM.createRoot(container); act(() => root.render( @@ -948,7 +1221,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); // $FlowFixMe - root = ReactDOM.unstable_createRoot(container); + root = ReactDOM.createRoot(container); act(() => root.render( @@ -1000,7 +1273,7 @@ describe('StoreStressConcurrent', () => { // Always start with a fresh container and steps[i]. container = document.createElement('div'); // $FlowFixMe - root = ReactDOM.unstable_createRoot(container); + root = ReactDOM.createRoot(container); act(() => root.render( diff --git a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js index c1f42f28f4bac..f29c079a6302c 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js @@ -47,557 +47,558 @@ describe('ReactDOMFiberAsync', () => { expect(ops).toEqual(['Hi', 'Bye']); }); - it('does not perform deferred updates synchronously', () => { - let inputRef = React.createRef(); - let asyncValueRef = React.createRef(); - let syncValueRef = React.createRef(); - - class Counter extends React.Component { - state = {asyncValue: '', syncValue: ''}; - - handleChange = e => { - const nextValue = e.target.value; - requestIdleCallback(() => { - this.setState({ - asyncValue: nextValue, - }); - // It should not be flushed yet. - expect(asyncValueRef.current.textContent).toBe(''); - }); - this.setState({ - syncValue: nextValue, - }); - }; - - render() { - return ( -
- -

{this.state.asyncValue}

-

{this.state.syncValue}

-
+ if (__EXPERIMENTAL__) { + describe('concurrent mode', () => { + beforeEach(() => { + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; + ReactDOM = require('react-dom'); + Scheduler = require('scheduler'); + }); + + it('does not perform deferred updates synchronously', () => { + let inputRef = React.createRef(); + let asyncValueRef = React.createRef(); + let syncValueRef = React.createRef(); + + class Counter extends React.Component { + state = {asyncValue: '', syncValue: ''}; + + handleChange = e => { + const nextValue = e.target.value; + requestIdleCallback(() => { + this.setState({ + asyncValue: nextValue, + }); + // It should not be flushed yet. + expect(asyncValueRef.current.textContent).toBe(''); + }); + this.setState({ + syncValue: nextValue, + }); + }; + + render() { + return ( +
+ +

{this.state.asyncValue}

+

{this.state.syncValue}

+
+ ); + } + } + const root = ReactDOM.createRoot(container); + root.render(); + Scheduler.unstable_flushAll(); + expect(asyncValueRef.current.textContent).toBe(''); + expect(syncValueRef.current.textContent).toBe(''); + + setUntrackedInputValue.call(inputRef.current, 'hello'); + inputRef.current.dispatchEvent( + new MouseEvent('input', {bubbles: true}), ); - } - } - const root = ReactDOM.unstable_createRoot(container); - root.render(); - Scheduler.unstable_flushAll(); - expect(asyncValueRef.current.textContent).toBe(''); - expect(syncValueRef.current.textContent).toBe(''); - - setUntrackedInputValue.call(inputRef.current, 'hello'); - inputRef.current.dispatchEvent(new MouseEvent('input', {bubbles: true})); - // Should only flush non-deferred update. - expect(asyncValueRef.current.textContent).toBe(''); - expect(syncValueRef.current.textContent).toBe('hello'); - - // Should flush both updates now. - jest.runAllTimers(); - Scheduler.unstable_flushAll(); - expect(asyncValueRef.current.textContent).toBe('hello'); - expect(syncValueRef.current.textContent).toBe('hello'); - }); + // Should only flush non-deferred update. + expect(asyncValueRef.current.textContent).toBe(''); + expect(syncValueRef.current.textContent).toBe('hello'); + + // Should flush both updates now. + jest.runAllTimers(); + Scheduler.unstable_flushAll(); + expect(asyncValueRef.current.textContent).toBe('hello'); + expect(syncValueRef.current.textContent).toBe('hello'); + }); - describe('concurrent mode', () => { - beforeEach(() => { - jest.resetModules(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; - ReactDOM = require('react-dom'); - Scheduler = require('scheduler'); - }); + it('top-level updates are concurrent', () => { + const root = ReactDOM.createRoot(container); + root.render(
Hi
); + expect(container.textContent).toEqual(''); + Scheduler.unstable_flushAll(); + expect(container.textContent).toEqual('Hi'); - it('top-level updates are concurrent', () => { - const root = ReactDOM.unstable_createRoot(container); - root.render(
Hi
); - expect(container.textContent).toEqual(''); - Scheduler.unstable_flushAll(); - expect(container.textContent).toEqual('Hi'); - - root.render(
Bye
); - expect(container.textContent).toEqual('Hi'); - Scheduler.unstable_flushAll(); - expect(container.textContent).toEqual('Bye'); - }); + root.render(
Bye
); + expect(container.textContent).toEqual('Hi'); + Scheduler.unstable_flushAll(); + expect(container.textContent).toEqual('Bye'); + }); - it('deep updates (setState) are concurrent', () => { - let instance; - class Component extends React.Component { - state = {step: 0}; - render() { - instance = this; - return
{this.state.step}
; + it('deep updates (setState) are concurrent', () => { + let instance; + class Component extends React.Component { + state = {step: 0}; + render() { + instance = this; + return
{this.state.step}
; + } } - } - - const root = ReactDOM.unstable_createRoot(container); - root.render(); - expect(container.textContent).toEqual(''); - Scheduler.unstable_flushAll(); - expect(container.textContent).toEqual('0'); - - instance.setState({step: 1}); - expect(container.textContent).toEqual('0'); - Scheduler.unstable_flushAll(); - expect(container.textContent).toEqual('1'); - }); - it('flushSync batches sync updates and flushes them at the end of the batch', () => { - let ops = []; - let instance; + const root = ReactDOM.createRoot(container); + root.render(); + expect(container.textContent).toEqual(''); + Scheduler.unstable_flushAll(); + expect(container.textContent).toEqual('0'); - class Component extends React.Component { - state = {text: ''}; - push(val) { - this.setState(state => ({text: state.text + val})); - } - componentDidUpdate() { - ops.push(this.state.text); - } - render() { - instance = this; - return {this.state.text}; - } - } + instance.setState({step: 1}); + expect(container.textContent).toEqual('0'); + Scheduler.unstable_flushAll(); + expect(container.textContent).toEqual('1'); + }); - ReactDOM.render(, container); + it('flushSync batches sync updates and flushes them at the end of the batch', () => { + let ops = []; + let instance; + + class Component extends React.Component { + state = {text: ''}; + push(val) { + this.setState(state => ({text: state.text + val})); + } + componentDidUpdate() { + ops.push(this.state.text); + } + render() { + instance = this; + return {this.state.text}; + } + } - instance.push('A'); - expect(ops).toEqual(['A']); - expect(container.textContent).toEqual('A'); + ReactDOM.render(, container); - ReactDOM.flushSync(() => { - instance.push('B'); - instance.push('C'); - // Not flushed yet - expect(container.textContent).toEqual('A'); + instance.push('A'); expect(ops).toEqual(['A']); - }); - expect(container.textContent).toEqual('ABC'); - expect(ops).toEqual(['A', 'ABC']); - instance.push('D'); - expect(container.textContent).toEqual('ABCD'); - expect(ops).toEqual(['A', 'ABC', 'ABCD']); - }); + expect(container.textContent).toEqual('A'); - it('flushSync flushes updates even if nested inside another flushSync', () => { - let ops = []; - let instance; + ReactDOM.flushSync(() => { + instance.push('B'); + instance.push('C'); + // Not flushed yet + expect(container.textContent).toEqual('A'); + expect(ops).toEqual(['A']); + }); + expect(container.textContent).toEqual('ABC'); + expect(ops).toEqual(['A', 'ABC']); + instance.push('D'); + expect(container.textContent).toEqual('ABCD'); + expect(ops).toEqual(['A', 'ABC', 'ABCD']); + }); - class Component extends React.Component { - state = {text: ''}; - push(val) { - this.setState(state => ({text: state.text + val})); + it('flushSync flushes updates even if nested inside another flushSync', () => { + let ops = []; + let instance; + + class Component extends React.Component { + state = {text: ''}; + push(val) { + this.setState(state => ({text: state.text + val})); + } + componentDidUpdate() { + ops.push(this.state.text); + } + render() { + instance = this; + return {this.state.text}; + } } - componentDidUpdate() { - ops.push(this.state.text); - } - render() { - instance = this; - return {this.state.text}; - } - } - - ReactDOM.render(, container); - instance.push('A'); - expect(ops).toEqual(['A']); - expect(container.textContent).toEqual('A'); + ReactDOM.render(, container); - ReactDOM.flushSync(() => { - instance.push('B'); - instance.push('C'); - // Not flushed yet - expect(container.textContent).toEqual('A'); + instance.push('A'); expect(ops).toEqual(['A']); + expect(container.textContent).toEqual('A'); ReactDOM.flushSync(() => { - instance.push('D'); + instance.push('B'); + instance.push('C'); + // Not flushed yet + expect(container.textContent).toEqual('A'); + expect(ops).toEqual(['A']); + + ReactDOM.flushSync(() => { + instance.push('D'); + }); + // The nested flushSync caused everything to flush. + expect(container.textContent).toEqual('ABCD'); + expect(ops).toEqual(['A', 'ABCD']); }); - // The nested flushSync caused everything to flush. expect(container.textContent).toEqual('ABCD'); expect(ops).toEqual(['A', 'ABCD']); }); - expect(container.textContent).toEqual('ABCD'); - expect(ops).toEqual(['A', 'ABCD']); - }); - it('flushSync throws if already performing work', () => { - class Component extends React.Component { - componentDidUpdate() { - ReactDOM.flushSync(() => {}); - } - render() { - return null; + it('flushSync throws if already performing work', () => { + class Component extends React.Component { + componentDidUpdate() { + ReactDOM.flushSync(() => {}); + } + render() { + return null; + } } - } - - // Initial mount - ReactDOM.render(, container); - // Update - expect(() => ReactDOM.render(, container)).toThrow( - 'flushSync was called from inside a lifecycle method', - ); - }); - it('flushSync flushes updates before end of the tick', () => { - let ops = []; - let instance; + // Initial mount + ReactDOM.render(, container); + // Update + expect(() => ReactDOM.render(, container)).toThrow( + 'flushSync was called from inside a lifecycle method', + ); + }); - class Component extends React.Component { - state = {text: ''}; - push(val) { - this.setState(state => ({text: state.text + val})); - } - componentDidUpdate() { - ops.push(this.state.text); - } - render() { - instance = this; - return {this.state.text}; + it('flushSync flushes updates before end of the tick', () => { + let ops = []; + let instance; + + class Component extends React.Component { + state = {text: ''}; + push(val) { + this.setState(state => ({text: state.text + val})); + } + componentDidUpdate() { + ops.push(this.state.text); + } + render() { + instance = this; + return {this.state.text}; + } } - } - const root = ReactDOM.unstable_createRoot(container); - root.render(); - Scheduler.unstable_flushAll(); + const root = ReactDOM.createRoot(container); + root.render(); + Scheduler.unstable_flushAll(); - // Updates are async by default - instance.push('A'); - expect(ops).toEqual([]); - expect(container.textContent).toEqual(''); - - ReactDOM.flushSync(() => { - instance.push('B'); - instance.push('C'); - // Not flushed yet - expect(container.textContent).toEqual(''); + // Updates are async by default + instance.push('A'); expect(ops).toEqual([]); + expect(container.textContent).toEqual(''); + + ReactDOM.flushSync(() => { + instance.push('B'); + instance.push('C'); + // Not flushed yet + expect(container.textContent).toEqual(''); + expect(ops).toEqual([]); + }); + // Only the active updates have flushed + expect(container.textContent).toEqual('BC'); + expect(ops).toEqual(['BC']); + + instance.push('D'); + expect(container.textContent).toEqual('BC'); + expect(ops).toEqual(['BC']); + + // Flush the async updates + Scheduler.unstable_flushAll(); + expect(container.textContent).toEqual('ABCD'); + expect(ops).toEqual(['BC', 'ABCD']); }); - // Only the active updates have flushed - expect(container.textContent).toEqual('BC'); - expect(ops).toEqual(['BC']); - - instance.push('D'); - expect(container.textContent).toEqual('BC'); - expect(ops).toEqual(['BC']); - - // Flush the async updates - Scheduler.unstable_flushAll(); - expect(container.textContent).toEqual('ABCD'); - expect(ops).toEqual(['BC', 'ABCD']); - }); - it('flushControlled flushes updates before yielding to browser', () => { - let inst; - class Counter extends React.Component { - state = {counter: 0}; - increment = () => - this.setState(state => ({counter: state.counter + 1})); - render() { - inst = this; - return this.state.counter; + it('flushControlled flushes updates before yielding to browser', () => { + let inst; + class Counter extends React.Component { + state = {counter: 0}; + increment = () => + this.setState(state => ({counter: state.counter + 1})); + render() { + inst = this; + return this.state.counter; + } } - } - const root = ReactDOM.unstable_createRoot(container); - root.render(); - Scheduler.unstable_flushAll(); - expect(container.textContent).toEqual('0'); - - // Test that a normal update is async - inst.increment(); - expect(container.textContent).toEqual('0'); - Scheduler.unstable_flushAll(); - expect(container.textContent).toEqual('1'); - - let ops = []; - ReactDOM.unstable_flushControlled(() => { + const root = ReactDOM.createRoot(container); + root.render(); + Scheduler.unstable_flushAll(); + expect(container.textContent).toEqual('0'); + + // Test that a normal update is async inst.increment(); + expect(container.textContent).toEqual('0'); + Scheduler.unstable_flushAll(); + expect(container.textContent).toEqual('1'); + + let ops = []; ReactDOM.unstable_flushControlled(() => { inst.increment(); - ops.push('end of inner flush: ' + container.textContent); + ReactDOM.unstable_flushControlled(() => { + inst.increment(); + ops.push('end of inner flush: ' + container.textContent); + }); + ops.push('end of outer flush: ' + container.textContent); }); - ops.push('end of outer flush: ' + container.textContent); + ops.push('after outer flush: ' + container.textContent); + expect(ops).toEqual([ + 'end of inner flush: 1', + 'end of outer flush: 1', + 'after outer flush: 3', + ]); }); - ops.push('after outer flush: ' + container.textContent); - expect(ops).toEqual([ - 'end of inner flush: 1', - 'end of outer flush: 1', - 'after outer flush: 3', - ]); - }); - it('flushControlled does not flush until end of outermost batchedUpdates', () => { - let inst; - class Counter extends React.Component { - state = {counter: 0}; - increment = () => - this.setState(state => ({counter: state.counter + 1})); - render() { - inst = this; - return this.state.counter; + it('flushControlled does not flush until end of outermost batchedUpdates', () => { + let inst; + class Counter extends React.Component { + state = {counter: 0}; + increment = () => + this.setState(state => ({counter: state.counter + 1})); + render() { + inst = this; + return this.state.counter; + } } - } - ReactDOM.render(, container); + ReactDOM.render(, container); - let ops = []; - ReactDOM.unstable_batchedUpdates(() => { - inst.increment(); - ReactDOM.unstable_flushControlled(() => { + let ops = []; + ReactDOM.unstable_batchedUpdates(() => { inst.increment(); - ops.push('end of flushControlled fn: ' + container.textContent); + ReactDOM.unstable_flushControlled(() => { + inst.increment(); + ops.push('end of flushControlled fn: ' + container.textContent); + }); + ops.push('end of batchedUpdates fn: ' + container.textContent); }); - ops.push('end of batchedUpdates fn: ' + container.textContent); + ops.push('after batchedUpdates: ' + container.textContent); + expect(ops).toEqual([ + 'end of flushControlled fn: 0', + 'end of batchedUpdates fn: 0', + 'after batchedUpdates: 2', + ]); }); - ops.push('after batchedUpdates: ' + container.textContent); - expect(ops).toEqual([ - 'end of flushControlled fn: 0', - 'end of batchedUpdates fn: 0', - 'after batchedUpdates: 2', - ]); - }); - it('flushControlled returns nothing', () => { - // In the future, we may want to return a thenable "work" object. - let inst; - class Counter extends React.Component { - state = {counter: 0}; - increment = () => - this.setState(state => ({counter: state.counter + 1})); - render() { - inst = this; - return this.state.counter; + it('flushControlled returns nothing', () => { + // In the future, we may want to return a thenable "work" object. + let inst; + class Counter extends React.Component { + state = {counter: 0}; + increment = () => + this.setState(state => ({counter: state.counter + 1})); + render() { + inst = this; + return this.state.counter; + } } - } - ReactDOM.render(, container); - expect(container.textContent).toEqual('0'); + ReactDOM.render(, container); + expect(container.textContent).toEqual('0'); - const returnValue = ReactDOM.unstable_flushControlled(() => { - inst.increment(); - return 'something'; + const returnValue = ReactDOM.unstable_flushControlled(() => { + inst.increment(); + return 'something'; + }); + expect(container.textContent).toEqual('1'); + expect(returnValue).toBe(undefined); }); - expect(container.textContent).toEqual('1'); - expect(returnValue).toBe(undefined); - }); - it('ignores discrete events on a pending removed element', () => { - const disableButtonRef = React.createRef(); - const submitButtonRef = React.createRef(); - - let formSubmitted = false; - - class Form extends React.Component { - state = {active: true}; - disableForm = () => { - this.setState({active: false}); - }; - submitForm = () => { - formSubmitted = true; // This should not get invoked - }; - render() { - return ( -
- - {this.state.active ? ( - - ) : null} -
- ); + {this.state.active ? ( + + ) : null} + + ); + } } - } - const root = ReactDOM.unstable_createRoot(container); - root.render(
); - // Flush - Scheduler.unstable_flushAll(); + const root = ReactDOM.createRoot(container); + root.render(); + // Flush + Scheduler.unstable_flushAll(); - let disableButton = disableButtonRef.current; - expect(disableButton.tagName).toBe('BUTTON'); + let disableButton = disableButtonRef.current; + expect(disableButton.tagName).toBe('BUTTON'); - // Dispatch a click event on the Disable-button. - let firstEvent = document.createEvent('Event'); - firstEvent.initEvent('click', true, true); - disableButton.dispatchEvent(firstEvent); + // Dispatch a click event on the Disable-button. + let firstEvent = document.createEvent('Event'); + firstEvent.initEvent('click', true, true); + disableButton.dispatchEvent(firstEvent); - // There should now be a pending update to disable the form. + // There should now be a pending update to disable the form. - // This should not have flushed yet since it's in concurrent mode. - let submitButton = submitButtonRef.current; - expect(submitButton.tagName).toBe('BUTTON'); + // This should not have flushed yet since it's in concurrent mode. + let submitButton = submitButtonRef.current; + expect(submitButton.tagName).toBe('BUTTON'); - // In the meantime, we can dispatch a new client event on the submit button. - let secondEvent = document.createEvent('Event'); - secondEvent.initEvent('click', true, true); - // This should force the pending update to flush which disables the submit button before the event is invoked. - submitButton.dispatchEvent(secondEvent); + // In the meantime, we can dispatch a new client event on the submit button. + let secondEvent = document.createEvent('Event'); + secondEvent.initEvent('click', true, true); + // This should force the pending update to flush which disables the submit button before the event is invoked. + submitButton.dispatchEvent(secondEvent); - // Therefore the form should never have been submitted. - expect(formSubmitted).toBe(false); + // Therefore the form should never have been submitted. + expect(formSubmitted).toBe(false); - expect(submitButtonRef.current).toBe(null); - }); + expect(submitButtonRef.current).toBe(null); + }); - it('ignores discrete events on a pending removed event listener', () => { - const disableButtonRef = React.createRef(); - const submitButtonRef = React.createRef(); - - let formSubmitted = false; - - class Form extends React.Component { - state = {active: true}; - disableForm = () => { - this.setState({active: false}); - }; - submitForm = () => { - formSubmitted = true; // This should not get invoked - }; - disabledSubmitForm = () => { - // The form is disabled. - }; - render() { - return ( -
- - -
- ); + it('ignores discrete events on a pending removed event listener', () => { + const disableButtonRef = React.createRef(); + const submitButtonRef = React.createRef(); + + let formSubmitted = false; + + class Form extends React.Component { + state = {active: true}; + disableForm = () => { + this.setState({active: false}); + }; + submitForm = () => { + formSubmitted = true; // This should not get invoked + }; + disabledSubmitForm = () => { + // The form is disabled. + }; + render() { + return ( +
+ + +
+ ); + } } - } - const root = ReactDOM.unstable_createRoot(container); - root.render(); - // Flush - Scheduler.unstable_flushAll(); + const root = ReactDOM.createRoot(container); + root.render(); + // Flush + Scheduler.unstable_flushAll(); - let disableButton = disableButtonRef.current; - expect(disableButton.tagName).toBe('BUTTON'); + let disableButton = disableButtonRef.current; + expect(disableButton.tagName).toBe('BUTTON'); - // Dispatch a click event on the Disable-button. - let firstEvent = document.createEvent('Event'); - firstEvent.initEvent('click', true, true); - disableButton.dispatchEvent(firstEvent); + // Dispatch a click event on the Disable-button. + let firstEvent = document.createEvent('Event'); + firstEvent.initEvent('click', true, true); + disableButton.dispatchEvent(firstEvent); - // There should now be a pending update to disable the form. + // There should now be a pending update to disable the form. - // This should not have flushed yet since it's in concurrent mode. - let submitButton = submitButtonRef.current; - expect(submitButton.tagName).toBe('BUTTON'); + // This should not have flushed yet since it's in concurrent mode. + let submitButton = submitButtonRef.current; + expect(submitButton.tagName).toBe('BUTTON'); - // In the meantime, we can dispatch a new client event on the submit button. - let secondEvent = document.createEvent('Event'); - secondEvent.initEvent('click', true, true); - // This should force the pending update to flush which disables the submit button before the event is invoked. - submitButton.dispatchEvent(secondEvent); + // In the meantime, we can dispatch a new client event on the submit button. + let secondEvent = document.createEvent('Event'); + secondEvent.initEvent('click', true, true); + // This should force the pending update to flush which disables the submit button before the event is invoked. + submitButton.dispatchEvent(secondEvent); - // Therefore the form should never have been submitted. - expect(formSubmitted).toBe(false); - }); + // Therefore the form should never have been submitted. + expect(formSubmitted).toBe(false); + }); - it('uses the newest discrete events on a pending changed event listener', () => { - const enableButtonRef = React.createRef(); - const submitButtonRef = React.createRef(); - - let formSubmitted = false; - - class Form extends React.Component { - state = {active: false}; - enableForm = () => { - this.setState({active: true}); - }; - submitForm = () => { - formSubmitted = true; // This should happen - }; - render() { - return ( -
- - {' '} - : null} -
- ); + it('uses the newest discrete events on a pending changed event listener', () => { + const enableButtonRef = React.createRef(); + const submitButtonRef = React.createRef(); + + let formSubmitted = false; + + class Form extends React.Component { + state = {active: false}; + enableForm = () => { + this.setState({active: true}); + }; + submitForm = () => { + formSubmitted = true; // This should happen + }; + render() { + return ( +
+ + {' '} + : null} +
+ ); + } } - } - const root = ReactDOM.unstable_createRoot(container); - root.render(); - // Flush - Scheduler.unstable_flushAll(); + const root = ReactDOM.createRoot(container); + root.render(); + // Flush + Scheduler.unstable_flushAll(); - let enableButton = enableButtonRef.current; - expect(enableButton.tagName).toBe('BUTTON'); + let enableButton = enableButtonRef.current; + expect(enableButton.tagName).toBe('BUTTON'); - // Dispatch a click event on the Enable-button. - let firstEvent = document.createEvent('Event'); - firstEvent.initEvent('click', true, true); - enableButton.dispatchEvent(firstEvent); + // Dispatch a click event on the Enable-button. + let firstEvent = document.createEvent('Event'); + firstEvent.initEvent('click', true, true); + enableButton.dispatchEvent(firstEvent); - // There should now be a pending update to enable the form. + // There should now be a pending update to enable the form. - // This should not have flushed yet since it's in concurrent mode. - let submitButton = submitButtonRef.current; - expect(submitButton.tagName).toBe('BUTTON'); + // This should not have flushed yet since it's in concurrent mode. + let submitButton = submitButtonRef.current; + expect(submitButton.tagName).toBe('BUTTON'); - // In the meantime, we can dispatch a new client event on the submit button. - let secondEvent = document.createEvent('Event'); - secondEvent.initEvent('click', true, true); - // This should force the pending update to flush which enables the submit button before the event is invoked. - submitButton.dispatchEvent(secondEvent); + // In the meantime, we can dispatch a new client event on the submit button. + let secondEvent = document.createEvent('Event'); + secondEvent.initEvent('click', true, true); + // This should force the pending update to flush which enables the submit button before the event is invoked. + submitButton.dispatchEvent(secondEvent); - // Therefore the form should have been submitted. - expect(formSubmitted).toBe(true); + // Therefore the form should have been submitted. + expect(formSubmitted).toBe(true); + }); }); - }); - describe('createSyncRoot', () => { - it('updates flush without yielding in the next event', () => { - const root = ReactDOM.unstable_createSyncRoot(container); - - function Text(props) { - Scheduler.unstable_yieldValue(props.text); - return props.text; - } - - root.render( - <> - - - - , - ); - - // Nothing should have rendered yet - expect(container.textContent).toEqual(''); - - // Everything should render immediately in the next event - expect(Scheduler).toFlushExpired(['A', 'B', 'C']); - expect(container.textContent).toEqual('ABC'); - }); + describe('createSyncRoot', () => { + it('updates flush without yielding in the next event', () => { + const root = ReactDOM.createSyncRoot(container); + + function Text(props) { + Scheduler.unstable_yieldValue(props.text); + return props.text; + } + + root.render( + <> + + + + , + ); + + // Nothing should have rendered yet + expect(container.textContent).toEqual(''); - it('does not support createBatch', () => { - const root = ReactDOM.unstable_createSyncRoot(container); - expect(root.createBatch).toBe(undefined); + // Everything should render immediately in the next event + expect(Scheduler).toFlushExpired(['A', 'B', 'C']); + expect(container.textContent).toEqual('ABC'); + }); }); - }); + } }); diff --git a/packages/react-dom/src/__tests__/ReactDOMHooks-test.js b/packages/react-dom/src/__tests__/ReactDOMHooks-test.js index d28ca5184d1a2..3276496202105 100644 --- a/packages/react-dom/src/__tests__/ReactDOMHooks-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMHooks-test.js @@ -105,38 +105,40 @@ describe('ReactDOMHooks', () => { expect(labelRef.current.innerHTML).toBe('abc'); }); - it('should not bail out when an update is scheduled from within an event handler in Concurrent Mode', () => { - const {createRef, useCallback, useState} = React; - - const Example = ({inputRef, labelRef}) => { - const [text, setText] = useState(''); - const handleInput = useCallback(event => { - setText(event.target.value); - }); - - return ( - <> - - - + if (__EXPERIMENTAL__) { + it('should not bail out when an update is scheduled from within an event handler in Concurrent Mode', () => { + const {createRef, useCallback, useState} = React; + + const Example = ({inputRef, labelRef}) => { + const [text, setText] = useState(''); + const handleInput = useCallback(event => { + setText(event.target.value); + }); + + return ( + <> + + + + ); + }; + + const inputRef = createRef(); + const labelRef = createRef(); + + const root = ReactDOM.createRoot(container); + root.render(); + + Scheduler.unstable_flushAll(); + + inputRef.current.value = 'abc'; + inputRef.current.dispatchEvent( + new Event('input', {bubbles: true, cancelable: true}), ); - }; - const inputRef = createRef(); - const labelRef = createRef(); - - const root = ReactDOM.unstable_createRoot(container); - root.render(); - - Scheduler.unstable_flushAll(); - - inputRef.current.value = 'abc'; - inputRef.current.dispatchEvent( - new Event('input', {bubbles: true, cancelable: true}), - ); + Scheduler.unstable_flushAll(); - Scheduler.unstable_flushAll(); - - expect(labelRef.current.innerHTML).toBe('abc'); - }); + expect(labelRef.current.innerHTML).toBe('abc'); + }); + } }); diff --git a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js index b58ae3508b696..1b77d32148195 100644 --- a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js @@ -26,15 +26,22 @@ describe('ReactDOMRoot', () => { Scheduler = require('scheduler'); }); + if (!__EXPERIMENTAL__) { + it('createRoot is not exposed in stable build', () => { + expect(ReactDOM.createRoot).toBe(undefined); + }); + return; + } + it('renders children', () => { - const root = ReactDOM.unstable_createRoot(container); + const root = ReactDOM.createRoot(container); root.render(
Hi
); Scheduler.unstable_flushAll(); expect(container.textContent).toEqual('Hi'); }); it('unmounts children', () => { - const root = ReactDOM.unstable_createRoot(container); + const root = ReactDOM.createRoot(container); root.render(
Hi
); Scheduler.unstable_flushAll(); expect(container.textContent).toEqual('Hi'); @@ -57,7 +64,7 @@ describe('ReactDOMRoot', () => { // Does not hydrate by default const container1 = document.createElement('div'); container1.innerHTML = markup; - const root1 = ReactDOM.unstable_createRoot(container1); + const root1 = ReactDOM.createRoot(container1); root1.render(
@@ -68,7 +75,7 @@ describe('ReactDOMRoot', () => { // Accepts `hydrate` option const container2 = document.createElement('div'); container2.innerHTML = markup; - const root2 = ReactDOM.unstable_createRoot(container2, {hydrate: true}); + const root2 = ReactDOM.createRoot(container2, {hydrate: true}); root2.render(
@@ -81,7 +88,7 @@ describe('ReactDOMRoot', () => { it('does not clear existing children', async () => { container.innerHTML = '
a
b
'; - const root = ReactDOM.unstable_createRoot(container); + const root = ReactDOM.createRoot(container); root.render(
c @@ -102,12 +109,12 @@ describe('ReactDOMRoot', () => { it('throws a good message on invalid containers', () => { expect(() => { - ReactDOM.unstable_createRoot(
Hi
); + ReactDOM.createRoot(
Hi
); }).toThrow('createRoot(...): Target container is not a DOM element.'); }); it('warns when rendering with legacy API into createRoot() container', () => { - const root = ReactDOM.unstable_createRoot(container); + const root = ReactDOM.createRoot(container); root.render(
Hi
); Scheduler.unstable_flushAll(); expect(container.textContent).toEqual('Hi'); @@ -130,7 +137,7 @@ describe('ReactDOMRoot', () => { }); it('warns when hydrating with legacy API into createRoot() container', () => { - const root = ReactDOM.unstable_createRoot(container); + const root = ReactDOM.createRoot(container); root.render(
Hi
); Scheduler.unstable_flushAll(); expect(container.textContent).toEqual('Hi'); @@ -150,7 +157,7 @@ describe('ReactDOMRoot', () => { }); it('warns when unmounting with legacy API (no previous content)', () => { - const root = ReactDOM.unstable_createRoot(container); + const root = ReactDOM.createRoot(container); root.render(
Hi
); Scheduler.unstable_flushAll(); expect(container.textContent).toEqual('Hi'); @@ -179,7 +186,7 @@ describe('ReactDOMRoot', () => { // Currently createRoot().render() doesn't clear this. container.appendChild(document.createElement('div')); // The rest is the same as test above. - const root = ReactDOM.unstable_createRoot(container); + const root = ReactDOM.createRoot(container); root.render(
Hi
); Scheduler.unstable_flushAll(); expect(container.textContent).toEqual('Hi'); @@ -198,7 +205,7 @@ describe('ReactDOMRoot', () => { it('warns when passing legacy container to createRoot()', () => { ReactDOM.render(
Hi
, container); expect(() => { - ReactDOM.unstable_createRoot(container); + ReactDOM.createRoot(container); }).toWarnDev( 'You are calling ReactDOM.createRoot() on a container that was previously ' + 'passed to ReactDOM.render(). This is not supported.', diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index dffe284c69b25..5c238f88b6592 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -75,7 +75,6 @@ describe('ReactDOMServerPartialHydration', () => { jest.resetModuleRegistry(); ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableSuspenseServerRenderer = true; ReactFeatureFlags.enableSuspenseCallback = true; ReactFeatureFlags.enableFlareAPI = true; @@ -90,6 +89,11 @@ describe('ReactDOMServerPartialHydration', () => { useHover = require('react-interactions/events/hover').useHover; }); + if (!__EXPERIMENTAL__) { + it("empty test so Jest doesn't complain", () => {}); + return; + } + it('hydrates a parent even if a child Suspense boundary is blocked', async () => { let suspend = false; let resolve; @@ -130,7 +134,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -200,7 +204,7 @@ describe('ReactDOMServerPartialHydration', () => { // hydrating anyway. suspend = true; suspend2 = true; - let root = ReactDOM.unstable_createRoot(container, { + let root = ReactDOM.createRoot(container, { hydrate: true, hydrationOptions: { onHydrated(node) { @@ -273,7 +277,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - let root = ReactDOM.unstable_createRoot(container, { + let root = ReactDOM.createRoot(container, { hydrate: true, hydrationOptions: { onDeleted(node) { @@ -411,7 +415,7 @@ describe('ReactDOMServerPartialHydration', () => { suspend = true; act(() => { - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); }); @@ -468,7 +472,7 @@ describe('ReactDOMServerPartialHydration', () => { // hydrating anyway. suspend = true; act(() => { - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); }); @@ -518,7 +522,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -587,7 +591,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -660,7 +664,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -732,7 +736,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -803,7 +807,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -889,7 +893,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render( @@ -971,7 +975,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render( @@ -1049,7 +1053,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we have the data available quickly for some reason. suspend = false; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -1105,7 +1109,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we have the data available quickly for some reason. suspend = false; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); // This will have exceeded the suspended time so we should timeout. @@ -1166,7 +1170,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we have the data available quickly for some reason. suspend = false; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); // This will have exceeded the suspended time so we should timeout. @@ -1242,7 +1246,7 @@ describe('ReactDOMServerPartialHydration', () => { // Attempt to hydrate the content. suspend = false; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -1335,7 +1339,7 @@ describe('ReactDOMServerPartialHydration', () => { // Attempt to hydrate the content. suspend = false; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -1413,7 +1417,7 @@ describe('ReactDOMServerPartialHydration', () => { let spanB = container.getElementsByTagName('span')[1]; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); suspend = true; act(() => { @@ -1495,7 +1499,7 @@ describe('ReactDOMServerPartialHydration', () => { let spanA = container.getElementsByTagName('span')[0]; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); suspend = true; act(() => { @@ -1575,7 +1579,7 @@ describe('ReactDOMServerPartialHydration', () => { // Put the suspense node in pending state. suspenseNode.data = '$?'; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); suspend = true; act(() => { @@ -1652,7 +1656,7 @@ describe('ReactDOMServerPartialHydration', () => { let span = container.getElementsByTagName('span')[1]; suspend = false; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -1695,7 +1699,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -1748,7 +1752,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render( @@ -1840,7 +1844,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -1914,7 +1918,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); // We'll do one click before hydrating. @@ -1995,7 +1999,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); // We'll do one click before hydrating. @@ -2072,7 +2076,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); // We'll do one click before hydrating. @@ -2151,7 +2155,7 @@ describe('ReactDOMServerPartialHydration', () => { // On the client we don't have all data yet but we want to start // hydrating anyway. suspend = true; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); jest.runAllTimers(); @@ -2216,7 +2220,7 @@ describe('ReactDOMServerPartialHydration', () => { // We're going to use a different root as a parent. // This lets us detect whether an event goes through React's event system. - let parentRoot = ReactDOM.unstable_createRoot(parentContainer); + let parentRoot = ReactDOM.createRoot(parentContainer); parentRoot.render(); Scheduler.unstable_flushAll(); @@ -2229,7 +2233,7 @@ describe('ReactDOMServerPartialHydration', () => { suspend = true; // Hydrate asynchronously. - let root = ReactDOM.unstable_createRoot(childContainer, {hydrate: true}); + let root = ReactDOM.createRoot(childContainer, {hydrate: true}); root.render(); jest.runAllTimers(); Scheduler.unstable_flushAll(); @@ -2319,7 +2323,7 @@ describe('ReactDOMServerPartialHydration', () => { // hydrating anyway. suspend1 = true; suspend2 = true; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); @@ -2434,7 +2438,7 @@ describe('ReactDOMServerPartialHydration', () => { // hydrating anyway. suspend1 = true; suspend2 = true; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); Scheduler.unstable_flushAll(); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js index e9983df3468ab..5b78d016ad564 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js @@ -13,7 +13,6 @@ let React; let ReactDOM; let ReactDOMServer; let Scheduler; -let ReactFeatureFlags; let Suspense; function dispatchMouseHoverEvent(to, from) { @@ -93,10 +92,6 @@ describe('ReactDOMServerSelectiveHydration', () => { beforeEach(() => { jest.resetModuleRegistry(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableSuspenseServerRenderer = true; - ReactFeatureFlags.enableSelectiveHydration = true; - React = require('react'); ReactDOM = require('react-dom'); ReactDOMServer = require('react-dom/server'); @@ -104,6 +99,11 @@ describe('ReactDOMServerSelectiveHydration', () => { Suspense = React.Suspense; }); + if (!__EXPERIMENTAL__) { + it("empty test so Jest doesn't complain", () => {}); + return; + } + it('hydrates the target boundary synchronously during a click', async () => { function Child({text}) { Scheduler.unstable_yieldValue(text); @@ -144,7 +144,7 @@ describe('ReactDOMServerSelectiveHydration', () => { let span = container.getElementsByTagName('span')[1]; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); // Nothing has been hydrated so far. @@ -223,7 +223,7 @@ describe('ReactDOMServerSelectiveHydration', () => { // A and D will be suspended. We'll click on D which should take // priority, after we unsuspend. - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); // Nothing has been hydrated so far. @@ -309,7 +309,7 @@ describe('ReactDOMServerSelectiveHydration', () => { // A and D will be suspended. We'll click on D which should take // priority, after we unsuspend. - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); // Nothing has been hydrated so far. @@ -405,7 +405,7 @@ describe('ReactDOMServerSelectiveHydration', () => { // A and D will be suspended. We'll click on D which should take // priority, after we unsuspend. - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); // Nothing has been hydrated so far. @@ -474,7 +474,7 @@ describe('ReactDOMServerSelectiveHydration', () => { let spanB = container.getElementsByTagName('span')[1]; let spanC = container.getElementsByTagName('span')[2]; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + let root = ReactDOM.createRoot(container, {hydrate: true}); root.render(); // Nothing has been hydrated so far. diff --git a/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js index 742bd4156533e..3d863169ccd3c 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js @@ -14,16 +14,12 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio let React; let ReactDOM; let ReactDOMServer; -let ReactFeatureFlags; let ReactTestUtils; function initModules() { // Reset warning cache. jest.resetModuleRegistry(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableSuspenseServerRenderer = true; - React = require('react'); ReactDOM = require('react-dom'); ReactDOMServer = require('react-dom/server'); @@ -48,6 +44,11 @@ describe('ReactDOMServerSuspense', () => { resetModules(); }); + if (!__EXPERIMENTAL__) { + it("empty test so Jest doesn't complain", () => {}); + return; + } + function Text(props) { return
{props.text}
; } @@ -125,7 +126,7 @@ describe('ReactDOMServerSuspense', () => { expect(divB.textContent).toBe('B'); ReactTestUtils.act(() => { - const root = ReactDOM.unstable_createSyncRoot(parent, {hydrate: true}); + const root = ReactDOM.createSyncRoot(parent, {hydrate: true}); root.render(example); }); diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.js index a1f48f3e495e0..54edfacb846dc 100644 --- a/packages/react-dom/src/__tests__/ReactServerRendering-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.js @@ -682,38 +682,40 @@ describe('ReactDOMServer', () => { expect(markup).toBe('
'); }); - it('throws for unsupported types on the server', () => { - expect(() => { - ReactDOMServer.renderToString(); - }).toThrow('ReactDOMServer does not yet support Suspense.'); + if (!__EXPERIMENTAL__) { + it('throws for unsupported types on the server', () => { + expect(() => { + ReactDOMServer.renderToString(); + }).toThrow('ReactDOMServer does not yet support Suspense.'); - async function fakeImport(result) { - return {default: result}; - } + async function fakeImport(result) { + return {default: result}; + } - expect(() => { - const LazyFoo = React.lazy(() => - fakeImport( - new Promise(resolve => - resolve(function Foo() { - return
; - }), + expect(() => { + const LazyFoo = React.lazy(() => + fakeImport( + new Promise(resolve => + resolve(function Foo() { + return
; + }), + ), ), - ), - ); - ReactDOMServer.renderToString(); - }).toThrow('ReactDOMServer does not yet support lazy-loaded components.'); - }); + ); + ReactDOMServer.renderToString(); + }).toThrow('ReactDOMServer does not yet support lazy-loaded components.'); + }); - it('throws when suspending on the server', () => { - function AsyncFoo() { - throw new Promise(() => {}); - } + it('throws when suspending on the server', () => { + function AsyncFoo() { + throw new Promise(() => {}); + } - expect(() => { - ReactDOMServer.renderToString(); - }).toThrow('ReactDOMServer does not yet support Suspense.'); - }); + expect(() => { + ReactDOMServer.renderToString(); + }).toThrow('ReactDOMServer does not yet support Suspense.'); + }); + } it('does not get confused by throwing null', () => { function Bad() { diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js index 1707f9f1dbdc5..b39feffb907c8 100644 --- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js @@ -500,165 +500,22 @@ describe('ReactDOMServerHydration', () => { expect(element.textContent).toBe('Hello world'); }); - it('does not re-enter hydration after committing the first one', () => { - let finalHTML = ReactDOMServer.renderToString(
); - let container = document.createElement('div'); - container.innerHTML = finalHTML; - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); - root.render(
); - Scheduler.unstable_flushAll(); - root.render(null); - Scheduler.unstable_flushAll(); - // This should not reenter hydration state and therefore not trigger hydration - // warnings. - root.render(
); - Scheduler.unstable_flushAll(); - }); - - it('does not invoke an event on a concurrent hydrating node until it commits', () => { - function Sibling({text}) { - Scheduler.unstable_yieldValue('Sibling'); - return Sibling; - } - - function Sibling2({text}) { - Scheduler.unstable_yieldValue('Sibling2'); - return null; - } - - let clicks = 0; - - function Button() { - Scheduler.unstable_yieldValue('Button'); - let [clicked, setClicked] = React.useState(false); - if (clicked) { - return null; - } - return ( - { - setClicked(true); - clicks++; - }}> - Click me - - ); - } - - function App() { - return ( -
-
- ); - } - - let finalHTML = ReactDOMServer.renderToString(); - let container = document.createElement('div'); - container.innerHTML = finalHTML; - expect(Scheduler).toHaveYielded(['Button', 'Sibling', 'Sibling2']); - - // We need this to be in the document since we'll dispatch events on it. - document.body.appendChild(container); - - let a = container.getElementsByTagName('a')[0]; - - // Hydrate asynchronously. - let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); - root.render(); - - // We haven't started hydrating yet. - a.click(); - // Clicking should not invoke the event yet because we haven't committed - // the hydration yet. - expect(clicks).toBe(0); - - // Flush part way through the render. - if (__DEV__) { - // In DEV effects gets double invoked. - expect(Scheduler).toFlushAndYieldThrough(['Button', 'Button', 'Sibling']); - } else { - expect(Scheduler).toFlushAndYieldThrough(['Button', 'Sibling']); - } - - expect(container.textContent).toBe('Click meSibling'); - - // We're now partially hydrated. - a.click(); - // Clicking should not invoke the event yet because we haven't committed - // the hydration yet. - expect(clicks).toBe(0); - - // Finish the rest of the hydration. - if (__DEV__) { - // In DEV effects gets double invoked. - expect(Scheduler).toFlushAndYield(['Sibling2', 'Button', 'Button']); - } else { - expect(Scheduler).toFlushAndYield(['Sibling2', 'Button']); - } - - // We should have picked up both events now. - expect(clicks).toBe(2); - - expect(container.textContent).toBe('Sibling'); - - document.body.removeChild(container); - }); - - it('does not invoke an event on a parent tree when a subtree is hydrating', () => { - let clicks = 0; - let childSlotRef = React.createRef(); - - function Parent() { - return
clicks++} ref={childSlotRef} />; - } - - function App() { - return ( - - ); - } - - let finalHTML = ReactDOMServer.renderToString(); - - let parentContainer = document.createElement('div'); - let childContainer = document.createElement('div'); - - // We need this to be in the document since we'll dispatch events on it. - document.body.appendChild(parentContainer); - - // We're going to use a different root as a parent. - // This lets us detect whether an event goes through React's event system. - let parentRoot = ReactDOM.unstable_createRoot(parentContainer); - parentRoot.render(); - Scheduler.unstable_flushAll(); - - childSlotRef.current.appendChild(childContainer); - - childContainer.innerHTML = finalHTML; - - let a = childContainer.getElementsByTagName('a')[0]; - - // Hydrate asynchronously. - let root = ReactDOM.unstable_createRoot(childContainer, {hydrate: true}); - root.render(); - // Nothing has rendered so far. - - a.click(); - expect(clicks).toBe(0); - - Scheduler.unstable_flushAll(); - - // We're now full hydrated. - - expect(clicks).toBe(1); - - document.body.removeChild(parentContainer); - }); + if (__EXPERIMENTAL__) { + it('does not re-enter hydration after committing the first one', () => { + let finalHTML = ReactDOMServer.renderToString(
); + let container = document.createElement('div'); + container.innerHTML = finalHTML; + let root = ReactDOM.createRoot(container, {hydrate: true}); + root.render(
); + Scheduler.unstable_flushAll(); + root.render(null); + Scheduler.unstable_flushAll(); + // This should not reenter hydration state and therefore not trigger hydration + // warnings. + root.render(
); + Scheduler.unstable_flushAll(); + }); + } it('regression test: Suspense + hydration in legacy mode ', () => { const element = document.createElement('div'); diff --git a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js index af7ad5d89cd24..4ff9533ed7301 100644 --- a/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js +++ b/packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js @@ -27,30 +27,32 @@ function sleep(period) { describe('ReactTestUtils.act()', () => { // first we run all the tests with concurrent mode - let concurrentRoot = null; - function renderConcurrent(el, dom) { - concurrentRoot = ReactDOM.unstable_createRoot(dom); - concurrentRoot.render(el); - } - - function unmountConcurrent(_dom) { - if (concurrentRoot !== null) { - concurrentRoot.unmount(); - concurrentRoot = null; - } - } - - function rerenderConcurrent(el) { - concurrentRoot.render(el); + if (__EXPERIMENTAL__) { + let concurrentRoot = null; + const renderConcurrent = (el, dom) => { + concurrentRoot = ReactDOM.createRoot(dom); + concurrentRoot.render(el); + }; + + const unmountConcurrent = _dom => { + if (concurrentRoot !== null) { + concurrentRoot.unmount(); + concurrentRoot = null; + } + }; + + const rerenderConcurrent = el => { + concurrentRoot.render(el); + }; + + runActTests( + 'concurrent mode', + renderConcurrent, + unmountConcurrent, + rerenderConcurrent, + ); } - runActTests( - 'concurrent mode', - renderConcurrent, - unmountConcurrent, - rerenderConcurrent, - ); - // and then in sync mode let syncDom = null; @@ -71,25 +73,27 @@ describe('ReactTestUtils.act()', () => { runActTests('legacy sync mode', renderSync, unmountSync, rerenderSync); // and then in batched mode - let batchedRoot = null; - function renderBatched(el, dom) { - batchedRoot = ReactDOM.unstable_createSyncRoot(dom); - batchedRoot.render(el); - } + if (__EXPERIMENTAL__) { + let batchedRoot = null; + const renderBatched = (el, dom) => { + batchedRoot = ReactDOM.createSyncRoot(dom); + batchedRoot.render(el); + }; + + const unmountBatched = dom => { + if (batchedRoot !== null) { + batchedRoot.unmount(); + batchedRoot = null; + } + }; - function unmountBatched(dom) { - if (batchedRoot !== null) { - batchedRoot.unmount(); - batchedRoot = null; - } - } + const rerenderBatched = el => { + batchedRoot.render(el); + }; - function rerenderBatched(el) { - batchedRoot.render(el); + runActTests('batched mode', renderBatched, unmountBatched, rerenderBatched); } - runActTests('batched mode', renderBatched, unmountBatched, rerenderBatched); - describe('unacted effects', () => { function App() { React.useEffect(() => {}, []); @@ -116,31 +120,29 @@ describe('ReactTestUtils.act()', () => { ]); }); - it('warns in batched mode', () => { - expect(() => { - const root = ReactDOM.unstable_createSyncRoot( - document.createElement('div'), - ); - root.render(); - Scheduler.unstable_flushAll(); - }).toWarnDev([ - 'An update to App ran an effect, but was not wrapped in act(...)', - 'An update to App ran an effect, but was not wrapped in act(...)', - ]); - }); + if (__EXPERIMENTAL__) { + it('warns in batched mode', () => { + expect(() => { + const root = ReactDOM.createSyncRoot(document.createElement('div')); + root.render(); + Scheduler.unstable_flushAll(); + }).toWarnDev([ + 'An update to App ran an effect, but was not wrapped in act(...)', + 'An update to App ran an effect, but was not wrapped in act(...)', + ]); + }); - it('warns in concurrent mode', () => { - expect(() => { - const root = ReactDOM.unstable_createRoot( - document.createElement('div'), - ); - root.render(); - Scheduler.unstable_flushAll(); - }).toWarnDev([ - 'An update to App ran an effect, but was not wrapped in act(...)', - 'An update to App ran an effect, but was not wrapped in act(...)', - ]); - }); + it('warns in concurrent mode', () => { + expect(() => { + const root = ReactDOM.createRoot(document.createElement('div')); + root.render(); + Scheduler.unstable_flushAll(); + }).toWarnDev([ + 'An update to App ran an effect, but was not wrapped in act(...)', + 'An update to App ran an effect, but was not wrapped in act(...)', + ]); + }); + } }); }); diff --git a/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js b/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js index ade05f823b64f..d7e2bb9c0cb25 100644 --- a/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js +++ b/packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js @@ -27,34 +27,32 @@ it('does not warn when rendering in sync mode', () => { }).toWarnDev([]); }); -it('should warn when rendering in concurrent mode', () => { - expect(() => { - ReactDOM.unstable_createRoot(document.createElement('div')).render(); - }).toWarnDev( - 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' + - 'to guarantee consistent behaviour across tests and browsers.', - {withoutStack: true}, - ); - // does not warn twice - expect(() => { - ReactDOM.unstable_createRoot(document.createElement('div')).render(); - }).toWarnDev([]); -}); - -it('should warn when rendering in batched mode', () => { - expect(() => { - ReactDOM.unstable_createSyncRoot(document.createElement('div')).render( - , +if (__EXPERIMENTAL__) { + it('should warn when rendering in concurrent mode', () => { + expect(() => { + ReactDOM.createRoot(document.createElement('div')).render(); + }).toWarnDev( + 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' + + 'to guarantee consistent behaviour across tests and browsers.', + {withoutStack: true}, ); - }).toWarnDev( - 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' + - 'to guarantee consistent behaviour across tests and browsers.', - {withoutStack: true}, - ); - // does not warn twice - expect(() => { - ReactDOM.unstable_createSyncRoot(document.createElement('div')).render( - , + // does not warn twice + expect(() => { + ReactDOM.createRoot(document.createElement('div')).render(); + }).toWarnDev([]); + }); + + it('should warn when rendering in batched mode', () => { + expect(() => { + ReactDOM.createSyncRoot(document.createElement('div')).render(); + }).toWarnDev( + 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' + + 'to guarantee consistent behaviour across tests and browsers.', + {withoutStack: true}, ); - }).toWarnDev([]); -}); + // does not warn twice + expect(() => { + ReactDOM.createSyncRoot(document.createElement('div')).render(); + }).toWarnDev([]); + }); +} diff --git a/packages/react-dom/src/__tests__/ReactUpdates-test.js b/packages/react-dom/src/__tests__/ReactUpdates-test.js index 8e23a6d5c1615..04e69d591e17c 100644 --- a/packages/react-dom/src/__tests__/ReactUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactUpdates-test.js @@ -1292,78 +1292,84 @@ describe('ReactUpdates', () => { expect(ops).toEqual(['Foo', 'Bar', 'Baz']); }); - it('delays sync updates inside hidden subtrees in Concurrent Mode', () => { - const container = document.createElement('div'); + if (__EXPERIMENTAL__) { + it('delays sync updates inside hidden subtrees in Concurrent Mode', () => { + const container = document.createElement('div'); - function Baz() { - Scheduler.unstable_yieldValue('Baz'); - return

baz

; - } + function Baz() { + Scheduler.unstable_yieldValue('Baz'); + return

baz

; + } - let setCounter; - function Bar() { - const [counter, _setCounter] = React.useState(0); - setCounter = _setCounter; - Scheduler.unstable_yieldValue('Bar'); - return

bar {counter}

; - } + let setCounter; + function Bar() { + const [counter, _setCounter] = React.useState(0); + setCounter = _setCounter; + Scheduler.unstable_yieldValue('Bar'); + return

bar {counter}

; + } - function Foo() { - Scheduler.unstable_yieldValue('Foo'); - React.useEffect(() => { - Scheduler.unstable_yieldValue('Foo#effect'); - }); - return ( -
- - ); - } - - const root = ReactDOM.unstable_createRoot(container); - let hiddenDiv; - act(() => { - root.render(); - if (__DEV__) { - expect(Scheduler).toFlushAndYieldThrough([ - 'Foo', - 'Foo', - 'Baz', - 'Foo#effect', - ]); - } else { - expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Baz', 'Foo#effect']); + ); } - hiddenDiv = container.firstChild.firstChild; - expect(hiddenDiv.hidden).toBe(true); - expect(hiddenDiv.innerHTML).toBe(''); + + const root = ReactDOM.createRoot(container); + let hiddenDiv; + act(() => { + root.render(); + if (__DEV__) { + expect(Scheduler).toFlushAndYieldThrough([ + 'Foo', + 'Foo', + 'Baz', + 'Foo#effect', + ]); + } else { + expect(Scheduler).toFlushAndYieldThrough([ + 'Foo', + 'Baz', + 'Foo#effect', + ]); + } + hiddenDiv = container.firstChild.firstChild; + expect(hiddenDiv.hidden).toBe(true); + expect(hiddenDiv.innerHTML).toBe(''); + // Run offscreen update + if (__DEV__) { + expect(Scheduler).toFlushAndYield(['Bar', 'Bar']); + } else { + expect(Scheduler).toFlushAndYield(['Bar']); + } + expect(hiddenDiv.hidden).toBe(true); + expect(hiddenDiv.innerHTML).toBe('

bar 0

'); + }); + + ReactDOM.flushSync(() => { + setCounter(1); + }); + // Should not flush yet + expect(hiddenDiv.innerHTML).toBe('

bar 0

'); + // Run offscreen update if (__DEV__) { expect(Scheduler).toFlushAndYield(['Bar', 'Bar']); } else { expect(Scheduler).toFlushAndYield(['Bar']); } - expect(hiddenDiv.hidden).toBe(true); - expect(hiddenDiv.innerHTML).toBe('

bar 0

'); + expect(hiddenDiv.innerHTML).toBe('

bar 1

'); }); - - ReactDOM.flushSync(() => { - setCounter(1); - }); - // Should not flush yet - expect(hiddenDiv.innerHTML).toBe('

bar 0

'); - - // Run offscreen update - if (__DEV__) { - expect(Scheduler).toFlushAndYield(['Bar', 'Bar']); - } else { - expect(Scheduler).toFlushAndYield(['Bar']); - } - expect(hiddenDiv.innerHTML).toBe('

bar 1

'); - }); + } it('can render ridiculously large number of roots without triggering infinite update loop error', () => { class Foo extends React.Component { diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js index b0fa8e2179daa..d4203ff3b69dd 100644 --- a/packages/react-dom/src/client/ReactDOM.js +++ b/packages/react-dom/src/client/ReactDOM.js @@ -61,7 +61,7 @@ import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; import lowPriorityWarningWithoutStack from 'shared/lowPriorityWarningWithoutStack'; import warningWithoutStack from 'shared/warningWithoutStack'; -import {enableStableConcurrentModeAPIs} from 'shared/ReactFeatureFlags'; +import {exposeConcurrentModeAPIs} from 'shared/ReactFeatureFlags'; import { getInstanceFromNode, @@ -593,27 +593,8 @@ const ReactDOM: Object = { unstable_batchedUpdates: batchedUpdates, - // TODO remove this legacy method, unstable_discreteUpdates replaces it - unstable_interactiveUpdates: (fn, a, b, c) => { - flushDiscreteUpdates(); - return discreteUpdates(fn, a, b, c); - }, - - unstable_discreteUpdates: discreteUpdates, - unstable_flushDiscreteUpdates: flushDiscreteUpdates, - flushSync: flushSync, - unstable_createRoot: createRoot, - unstable_createSyncRoot: createSyncRoot, - unstable_flushControlled: flushControlled, - - unstable_scheduleHydration(target: Node) { - if (target) { - queueExplicitHydrationTarget(target); - } - }, - __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: { // Keep in sync with ReactDOMUnstableNativeDependencies.js // ReactTestUtils.js, and ReactTestUtilsAct.js. This is an array for better minification. @@ -643,7 +624,7 @@ type RootOptions = { }, }; -function createRoot( +export function createRoot( container: DOMContainer, options?: RootOptions, ): _ReactRoot { @@ -655,7 +636,7 @@ function createRoot( return new ReactRoot(container, options); } -function createSyncRoot( +export function createSyncRoot( container: DOMContainer, options?: RootOptions, ): _ReactRoot { @@ -678,9 +659,19 @@ function warnIfReactDOMContainerInDEV(container) { } } -if (enableStableConcurrentModeAPIs) { +if (exposeConcurrentModeAPIs) { ReactDOM.createRoot = createRoot; ReactDOM.createSyncRoot = createSyncRoot; + + ReactDOM.unstable_discreteUpdates = discreteUpdates; + ReactDOM.unstable_flushDiscreteUpdates = flushDiscreteUpdates; + ReactDOM.unstable_flushControlled = flushControlled; + + ReactDOM.unstable_scheduleHydration = target => { + if (target) { + queueExplicitHydrationTarget(target); + } + }; } const foundDevTools = injectIntoDevTools({ diff --git a/packages/react-dom/src/client/ReactDOMFB.js b/packages/react-dom/src/client/ReactDOMFB.js index 83353421e4ae0..88c0e5636dabe 100644 --- a/packages/react-dom/src/client/ReactDOMFB.js +++ b/packages/react-dom/src/client/ReactDOMFB.js @@ -12,9 +12,35 @@ import {getIsHydrating} from 'react-reconciler/src/ReactFiberHydrationContext'; import {get as getInstance} from 'shared/ReactInstanceMap'; import {addUserTimingListener} from 'shared/ReactFeatureFlags'; -import ReactDOM from './ReactDOM'; +import ReactDOM, {createRoot, createSyncRoot} from './ReactDOM'; import {isEnabled} from '../events/ReactBrowserEventEmitter'; import {getClosestInstanceFromNode} from './ReactDOMComponentTree'; +import {queueExplicitHydrationTarget} from '../events/ReactDOMEventReplaying'; + +import { + discreteUpdates, + flushDiscreteUpdates, + flushControlled, +} from 'react-reconciler/inline.dom'; + +// TODO remove this legacy method, unstable_discreteUpdates replaces it +ReactDOM.unstable_interactiveUpdates = (fn, a, b, c) => { + flushDiscreteUpdates(); + return discreteUpdates(fn, a, b, c); +}; + +ReactDOM.unstable_discreteUpdates = discreteUpdates; +ReactDOM.unstable_flushDiscreteUpdates = flushDiscreteUpdates; +ReactDOM.unstable_flushControlled = flushControlled; + +ReactDOM.unstable_createRoot = createRoot; +ReactDOM.unstable_createSyncRoot = createSyncRoot; + +ReactDOM.unstable_scheduleHydration = target => { + if (target) { + queueExplicitHydrationTarget(target); + } +}; Object.assign( (ReactDOM.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: any), diff --git a/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js b/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js index ab8b5ee246a2c..532f2216a2c73 100644 --- a/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js +++ b/packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js @@ -474,319 +474,321 @@ describe('ChangeEventPlugin', () => { } }); - describe('concurrent mode', () => { - beforeEach(() => { - jest.resetModules(); - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; - React = require('react'); - ReactDOM = require('react-dom'); - TestUtils = require('react-dom/test-utils'); - Scheduler = require('scheduler'); - }); + if (__EXPERIMENTAL__) { + describe('concurrent mode', () => { + beforeEach(() => { + jest.resetModules(); + ReactFeatureFlags = require('shared/ReactFeatureFlags'); + ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false; + React = require('react'); + ReactDOM = require('react-dom'); + TestUtils = require('react-dom/test-utils'); + Scheduler = require('scheduler'); + }); - it('text input', () => { - const root = ReactDOM.unstable_createRoot(container); - let input; + it('text input', () => { + const root = ReactDOM.createRoot(container); + let input; - let ops = []; + let ops = []; - class ControlledInput extends React.Component { - state = {value: 'initial'}; - onChange = event => this.setState({value: event.target.value}); - render() { - ops.push(`render: ${this.state.value}`); - const controlledValue = - this.state.value === 'changed' ? 'changed [!]' : this.state.value; - return ( - (input = el)} - type="text" - value={controlledValue} - onChange={this.onChange} - /> - ); + class ControlledInput extends React.Component { + state = {value: 'initial'}; + onChange = event => this.setState({value: event.target.value}); + render() { + ops.push(`render: ${this.state.value}`); + const controlledValue = + this.state.value === 'changed' ? 'changed [!]' : this.state.value; + return ( + (input = el)} + type="text" + value={controlledValue} + onChange={this.onChange} + /> + ); + } } - } - // Initial mount. Test that this is async. - root.render(); - // Should not have flushed yet. - expect(ops).toEqual([]); - expect(input).toBe(undefined); - // Flush callbacks. - Scheduler.unstable_flushAll(); - expect(ops).toEqual(['render: initial']); - expect(input.value).toBe('initial'); - - ops = []; - - // Trigger a change event. - setUntrackedValue.call(input, 'changed'); - input.dispatchEvent( - new Event('input', {bubbles: true, cancelable: true}), - ); - // Change should synchronously flush - expect(ops).toEqual(['render: changed']); - // Value should be the controlled value, not the original one - expect(input.value).toBe('changed [!]'); - }); + // Initial mount. Test that this is async. + root.render(); + // Should not have flushed yet. + expect(ops).toEqual([]); + expect(input).toBe(undefined); + // Flush callbacks. + Scheduler.unstable_flushAll(); + expect(ops).toEqual(['render: initial']); + expect(input.value).toBe('initial'); + + ops = []; + + // Trigger a change event. + setUntrackedValue.call(input, 'changed'); + input.dispatchEvent( + new Event('input', {bubbles: true, cancelable: true}), + ); + // Change should synchronously flush + expect(ops).toEqual(['render: changed']); + // Value should be the controlled value, not the original one + expect(input.value).toBe('changed [!]'); + }); - it('checkbox input', () => { - const root = ReactDOM.unstable_createRoot(container); - let input; - - let ops = []; - - class ControlledInput extends React.Component { - state = {checked: false}; - onChange = event => { - this.setState({checked: event.target.checked}); - }; - render() { - ops.push(`render: ${this.state.checked}`); - const controlledValue = this.props.reverse - ? !this.state.checked - : this.state.checked; - return ( - (input = el)} - type="checkbox" - checked={controlledValue} - onChange={this.onChange} - /> - ); + it('checkbox input', () => { + const root = ReactDOM.createRoot(container); + let input; + + let ops = []; + + class ControlledInput extends React.Component { + state = {checked: false}; + onChange = event => { + this.setState({checked: event.target.checked}); + }; + render() { + ops.push(`render: ${this.state.checked}`); + const controlledValue = this.props.reverse + ? !this.state.checked + : this.state.checked; + return ( + (input = el)} + type="checkbox" + checked={controlledValue} + onChange={this.onChange} + /> + ); + } } - } - - // Initial mount. Test that this is async. - root.render(); - // Should not have flushed yet. - expect(ops).toEqual([]); - expect(input).toBe(undefined); - // Flush callbacks. - Scheduler.unstable_flushAll(); - expect(ops).toEqual(['render: false']); - expect(input.checked).toBe(false); - - ops = []; - - // Trigger a change event. - input.dispatchEvent( - new MouseEvent('click', {bubbles: true, cancelable: true}), - ); - // Change should synchronously flush - expect(ops).toEqual(['render: true']); - expect(input.checked).toBe(true); - // Now let's make sure we're using the controlled value. - root.render(); - Scheduler.unstable_flushAll(); - - ops = []; + // Initial mount. Test that this is async. + root.render(); + // Should not have flushed yet. + expect(ops).toEqual([]); + expect(input).toBe(undefined); + // Flush callbacks. + Scheduler.unstable_flushAll(); + expect(ops).toEqual(['render: false']); + expect(input.checked).toBe(false); + + ops = []; + + // Trigger a change event. + input.dispatchEvent( + new MouseEvent('click', {bubbles: true, cancelable: true}), + ); + // Change should synchronously flush + expect(ops).toEqual(['render: true']); + expect(input.checked).toBe(true); - // Trigger another change event. - input.dispatchEvent( - new MouseEvent('click', {bubbles: true, cancelable: true}), - ); - // Change should synchronously flush - expect(ops).toEqual(['render: true']); - expect(input.checked).toBe(false); - }); + // Now let's make sure we're using the controlled value. + root.render(); + Scheduler.unstable_flushAll(); - it('textarea', () => { - const root = ReactDOM.unstable_createRoot(container); - let textarea; + ops = []; - let ops = []; + // Trigger another change event. + input.dispatchEvent( + new MouseEvent('click', {bubbles: true, cancelable: true}), + ); + // Change should synchronously flush + expect(ops).toEqual(['render: true']); + expect(input.checked).toBe(false); + }); - class ControlledTextarea extends React.Component { - state = {value: 'initial'}; - onChange = event => this.setState({value: event.target.value}); - render() { - ops.push(`render: ${this.state.value}`); - const controlledValue = - this.state.value === 'changed' ? 'changed [!]' : this.state.value; - return ( -