Repository: facebook/react
Pull request: Convert ReactMount to createRoot

To convert this file, I started replacing all the calls in line, and quickly realized that we already have most of these tests covered in other files. So I found the test that we didn't already have for `create/hydrateRoot` and added them, then renamed `ReactMount` to `ReactLegacyMount`.

Changed files:
filename: packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js
patch: @@ -113,6 +113,43 @@ describe('ReactDOMServerHydration', () => {
       }
     });
 
+    // @gate __DEV__
+    it('warns when escaping on a checksum mismatch', () => {
+      function Mismatch({isClient}) {
+        if (isClient) {
+          return (
+            <div>This markup contains an nbsp entity: &nbsp; client text</div>
+          );
+        }
+        return (
+          <div>This markup contains an nbsp entity: &nbsp; server text</div>
+        );
+      }
+
+      /* eslint-disable no-irregular-whitespace */
+      if (gate(flags => flags.enableClientRenderFallbackOnTextMismatch)) {
+        expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
+          [
+            "Warning: Text content did not match. Server: "This markup contains an nbsp entity:   server text" Client: "This markup contains an nbsp entity:   client text"
+              in div (at **)
+              in Mismatch (at **)",
+            "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
+            "Caught [Text content does not match server-rendered HTML.]",
+            "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
+          ]
+        `);
+      } else {
+        expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
+          [
+            "Warning: Text content did not match. Server: "This markup contains an nbsp entity:   server text" Client: "This markup contains an nbsp entity:   client text"
+              in div (at **)
+              in Mismatch (at **)",
+          ]
+        `);
+      }
+      /* eslint-enable no-irregular-whitespace */
+    });
+
     // @gate __DEV__
     it('warns when client and server render different html', () => {
       function Mismatch({isClient}) {

filename: packages/react-dom/src/__tests__/ReactDOMRoot-test.js
patch: @@ -175,27 +175,6 @@ describe('ReactDOMRoot', () => {
     );
   });
 
-  it('clears existing children with legacy API', async () => {
-    container.innerHTML = '<div>a</div><div>b</div>';
-    ReactDOM.render(
-      <div>
-        <span>c</span>
-        <span>d</span>
-      </div>,
-      container,
-    );
-    expect(container.textContent).toEqual('cd');
-    ReactDOM.render(
-      <div>
-        <span>d</span>
-        <span>c</span>
-      </div>,
-      container,
-    );
-    await waitForAll([]);
-    expect(container.textContent).toEqual('dc');
-  });
-
   it('clears existing children', async () => {
     container.innerHTML = '<div>a</div><div>b</div>';
     const root = ReactDOMClient.createRoot(container);
@@ -223,122 +202,6 @@ describe('ReactDOMRoot', () => {
     }).toThrow('createRoot(...): Target container is not a DOM element.');
   });
 
-  it('warns when rendering with legacy API into createRoot() container', async () => {
-    const root = ReactDOMClient.createRoot(container);
-    root.render(<div>Hi</div>);
-    await waitForAll([]);
-    expect(container.textContent).toEqual('Hi');
-    expect(() => {
-      ReactDOM.render(<div>Bye</div>, container);
-    }).toErrorDev(
-      [
-        // We care about this warning:
-        'You are calling ReactDOM.render() on a container that was previously ' +
-          'passed to ReactDOMClient.createRoot(). This is not supported. ' +
-          'Did you mean to call root.render(element)?',
-        // This is more of a symptom but restructuring the code to avoid it isn't worth it:
-        'Replacing React-rendered children with a new root component.',
-      ],
-      {withoutStack: true},
-    );
-    await waitForAll([]);
-    // This works now but we could disallow it:
-    expect(container.textContent).toEqual('Bye');
-  });
-
-  it('warns when hydrating with legacy API into createRoot() container', async () => {
-    const root = ReactDOMClient.createRoot(container);
-    root.render(<div>Hi</div>);
-    await waitForAll([]);
-    expect(container.textContent).toEqual('Hi');
-    expect(() => {
-      ReactDOM.hydrate(<div>Hi</div>, container);
-    }).toErrorDev(
-      [
-        // We care about this warning:
-        'You are calling ReactDOM.hydrate() on a container that was previously ' +
-          'passed to ReactDOMClient.createRoot(). This is not supported. ' +
-          'Did you mean to call hydrateRoot(container, element)?',
-        // This is more of a symptom but restructuring the code to avoid it isn't worth it:
-        'Replacing React-rendered children with a new root component.',
-      ],
-      {withoutStack: true},
-    );
-  });
-
-  it('callback passed to legacy hydrate() API', () => {
-    container.innerHTML = '<div>Hi</div>';
-    ReactDOM.hydrate(<div>Hi</div>, container, () => {
-      Scheduler.log('callback');
-    });
-    expect(container.textContent).toEqual('Hi');
-    assertLog(['callback']);
-  });
-
-  it('warns when unmounting with legacy API (no previous content)', async () => {
-    const root = ReactDOMClient.createRoot(container);
-    root.render(<div>Hi</div>);
-    await waitForAll([]);
-    expect(container.textContent).toEqual('Hi');
-    let unmounted = false;
-    expect(() => {
-      unmounted = ReactDOM.unmountComponentAtNode(container);
-    }).toErrorDev(
-      [
-        // We care about this warning:
-        'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' +
-          'passed to ReactDOMClient.createRoot(). This is not supported. Did you mean to call root.unmount()?',
-        // This is more of a symptom but restructuring the code to avoid it isn't worth it:
-        "The node you're attempting to unmount was rendered by React and is not a top-level container.",
-      ],
-      {withoutStack: true},
-    );
-    expect(unmounted).toBe(false);
-    await waitForAll([]);
-    expect(container.textContent).toEqual('Hi');
-    root.unmount();
-    await waitForAll([]);
-    expect(container.textContent).toEqual('');
-  });
-
-  it('warns when unmounting with legacy API (has previous content)', async () => {
-    // Currently createRoot().render() doesn't clear this.
-    container.appendChild(document.createElement('div'));
-    // The rest is the same as test above.
-    const root = ReactDOMClient.createRoot(container);
-    root.render(<div>Hi</div>);
-    await waitForAll([]);
-    expect(container.textContent).toEqual('Hi');
-    let unmounted = false;
-    expect(() => {
-      unmounted = ReactDOM.unmountComponentAtNode(container);
-    }).toErrorDev(
-      [
-        'Did you mean to call root.unmount()?',
-        // This is more of a symptom but restructuring the code to avoid it isn't worth it:
-        "The node you're attempting to unmount was rendered by React and is not a top-level container.",
-      ],
-      {withoutStack: true},
-    );
-    expect(unmounted).toBe(false);
-    await waitForAll([]);
-    expect(container.textContent).toEqual('Hi');
-    root.unmount();
-    await waitForAll([]);
-    expect(container.textContent).toEqual('');
-  });
-
-  it('warns when passing legacy container to createRoot()', () => {
-    ReactDOM.render(<div>Hi</div>, container);
-    expect(() => {
-      ReactDOMClient.createRoot(container);
-    }).toErrorDev(
-      'You are calling ReactDOMClient.createRoot() on a container that was previously ' +
-        'passed to ReactDOM.render(). This is not supported.',
-      {withoutStack: true},
-    );
-  });
-
   it('warns when creating two roots managing the same container', () => {
     ReactDOMClient.createRoot(container);
     expect(() => {
@@ -399,6 +262,80 @@ describe('ReactDOMRoot', () => {
     }
   });
 
+  it('should render different components in same root', async () => {
+    document.body.appendChild(container);
+    const root = ReactDOMClient.createRoot(container);
+
+    await act(() => {
+      root.render(<div />);
+    });
+    expect(container.firstChild.nodeName).toBe('DIV');
+
+    await act(() => {
+      root.render(<span />);
+    });
+    expect(container.firstChild.nodeName).toBe('SPAN');
+  });
+
+  it('should not warn if mounting into non-empty node', async () => {
+    container.innerHTML = '<div></div>';
+    const root = ReactDOMClient.createRoot(container);
+    await act(() => {
+      root.render(<div />);
+    });
+
+    expect(true).toBe(true);
+  });
+
+  it('should reuse markup if rendering to the same target twice', async () => {
+    const root = ReactDOMClient.createRoot(container);
+    await act(() => {
+      root.render(<div />);
+    });
+    const firstElm = container.firstChild;
+    await act(() => {
+      root.render(<div />);
+    });
+
+    expect(firstElm).toBe(container.firstChild);
+  });
+
+  it('should unmount and remount if the key changes', async () => {
+    function Component({text}) {
+      useEffect(() => {
+        Scheduler.log('Mount');
+
+        return () => {
+          Scheduler.log('Unmount');
+        };
+      }, []);
+
+      return <span>{text}</span>;
+    }
+
+    const root = ReactDOMClient.createRoot(container);
+
+    await act(() => {
+      root.render(<Component text="orange" key="A" />);
+    });
+    expect(container.firstChild.innerHTML).toBe('orange');
+    assertLog(['Mount']);
+
+    // If we change the key, the component is unmounted and remounted
+    await act(() => {
+      root.render(<Component text="green" key="B" />);
+    });
+    expect(container.firstChild.innerHTML).toBe('green');
+    assertLog(['Unmount', 'Mount']);
+
+    // But if we don't change the key, the component instance is reused
+    await act(() => {
+      root.render(<Component text="blue" key="B" />);
+    });
+    expect(container.firstChild.innerHTML).toBe('blue');
+    assertLog([]);
+  });
+
   it('throws if unmounting a root that has had its contents removed', async () => {
     const root = ReactDOMClient.createRoot(container);
     await act(() => {
@@ -514,9 +451,6 @@ describe('ReactDOMRoot', () => {
     expect(() => ReactDOMClient.hydrateRoot(commentNode)).toThrow(
       'hydrateRoot(...): Target container is not a DOM element.',
     );
-
-    // Still works in the legacy API
-    ReactDOM.render(<div />, commentNode);
   });
 
   it('warn if no children passed to hydrateRoot', async () => {
@@ -539,4 +473,23 @@ describe('ReactDOMRoot', () => {
       },
     );
   });
+
+  it('warns when given a function', () => {
+    function Component() {
+      return <div />;
+    }
+
+    const root = ReactDOMClient.createRoot(document.createElement('div'));
+
+    expect(() => {
+      ReactDOM.flushSync(() => {
+        root.render(Component);
+      });
+    }).toErrorDev(
+      'Functions are not valid as a React child. ' +
+        'This may happen if you return a Component instead of <Component /> from render. ' +
+        'Or maybe you meant to call this function rather than return it.',
+      {withoutStack: true},
+    );
+  });
 });

filename: packages/react-dom/src/__tests__/ReactLegacyMount-test.js
patch: @@ -15,15 +15,25 @@ let React;
 let ReactDOM;
 let ReactDOMServer;
 let ReactTestUtils;
+let Scheduler;
+let ReactDOMClient;
+let assertLog;
+let waitForAll;
 
 describe('ReactMount', () => {
   beforeEach(() => {
     jest.resetModules();
 
     React = require('react');
     ReactDOM = require('react-dom');
+    ReactDOMClient = require('react-dom/client');
     ReactDOMServer = require('react-dom/server');
     ReactTestUtils = require('react-dom/test-utils');
+    Scheduler = require('scheduler');
+
+    const InternalTestUtils = require('internal-test-utils');
+    assertLog = InternalTestUtils.assertLog;
+    waitForAll = InternalTestUtils.waitForAll;
   });
 
   describe('unmountComponentAtNode', () => {
@@ -337,4 +347,127 @@ describe('ReactMount', () => {
       );
     });
   });
+
+  it('clears existing children with legacy API', async () => {
+    const container = document.createElement('div');
+    container.innerHTML = '<div>a</div><div>b</div>';
+    ReactDOM.render(
+      <div>
+        <span>c</span>
+        <span>d</span>
+      </div>,
+      container,
+    );
+    expect(container.textContent).toEqual('cd');
+    ReactDOM.render(
+      <div>
+        <span>d</span>
+        <span>c</span>
+      </div>,
+      container,
+    );
+    await waitForAll([]);
+    expect(container.textContent).toEqual('dc');
+  });
+
+  it('warns when rendering with legacy API into createRoot() container', async () => {
+    const container = document.createElement('div');
+    const root = ReactDOMClient.createRoot(container);
+    root.render(<div>Hi</div>);
+    await waitForAll([]);
+    expect(container.textContent).toEqual('Hi');
+    expect(() => {
+      ReactDOM.render(<div>Bye</div>, container);
+    }).toErrorDev(
+      [
+        // We care about this warning:
+        'You are calling ReactDOM.render() on a container that was previously ' +
+          'passed to ReactDOMClient.createRoot(). This is not supported. ' +
+          'Did you mean to call root.render(element)?',
+        // This is more of a symptom but restructuring the code to avoid it isn't worth it:
+        'Replacing React-rendered children with a new root component.',
+      ],
+      {withoutStack: true},
+    );
+    await waitForAll([]);
+    // This works now but we could disallow it:
+    expect(container.textContent).toEqual('Bye');
+  });
+
+  it('callback passed to legacy hydrate() API', () => {
+    const container = document.createElement('div');
+    container.innerHTML = '<div>Hi</div>';
+    ReactDOM.hydrate(<div>Hi</div>, container, () => {
+      Scheduler.log('callback');
+    });
+    expect(container.textContent).toEqual('Hi');
+    assertLog(['callback']);
+  });
+
+  it('warns when unmounting with legacy API (no previous content)', async () => {
+    const container = document.createElement('div');
+    const root = ReactDOMClient.createRoot(container);
+    root.render(<div>Hi</div>);
+    await waitForAll([]);
+    expect(container.textContent).toEqual('Hi');
+    let unmounted = false;
+    expect(() => {
+      unmounted = ReactDOM.unmountComponentAtNode(container);
+    }).toErrorDev(
+      [
+        // We care about this warning:
+        'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' +
+          'passed to ReactDOMClient.createRoot(). This is not supported. Did you mean to call root.unmount()?',
+        // This is more of a symptom but restructuring the code to avoid it isn't worth it:
+        "The node you're attempting to unmount was rendered by React and is not a top-level container.",
+      ],
+      {withoutStack: true},
+    );
+    expect(unmounted).toBe(false);
+    await waitForAll([]);
+    expect(container.textContent).toEqual('Hi');
+    root.unmount();
+    await waitForAll([]);
+    expect(container.textContent).toEqual('');
+  });
+
+  it('warns when unmounting with legacy API (has previous content)', async () => {
+    const container = document.createElement('div');
+    // Currently createRoot().render() doesn't clear this.
+    container.appendChild(document.createElement('div'));
+    // The rest is the same as test above.
+    const root = ReactDOMClient.createRoot(container);
+    root.render(<div>Hi</div>);
+    await waitForAll([]);
+    expect(container.textContent).toEqual('Hi');
+    let unmounted = false;
+    expect(() => {
+      unmounted = ReactDOM.unmountComponentAtNode(container);
+    }).toErrorDev(
+      [
+        'Did you mean to call root.unmount()?',
+        // This is more of a symptom but restructuring the code to avoid it isn't worth it:
+        "The node you're attempting to unmount was rendered by React and is not a top-level container.",
+      ],
+      {withoutStack: true},
+    );
+    expect(unmounted).toBe(false);
+    await waitForAll([]);
+    expect(container.textContent).toEqual('Hi');
+    root.unmount();
+    await waitForAll([]);
+    expect(container.textContent).toEqual('');
+  });
+
+  it('warns when passing legacy container to createRoot()', () => {
+    const container = document.createElement('div');
+    ReactDOM.render(<div>Hi</div>, container);
+    expect(() => {
+      ReactDOMClient.createRoot(container);
+    }).toErrorDev(
+      'You are calling ReactDOMClient.createRoot() on a container that was previously ' +
+        'passed to ReactDOM.render(). This is not supported.',
+      {withoutStack: true},
+    );
+  });
 });