1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @emails react-core
    
  8.  */
    
  9. 
    
  10. 'use strict';
    
  11. 
    
  12. let React = require('react');
    
  13. let ReactDOM = require('react-dom');
    
  14. let ReactDOMClient = require('react-dom/client');
    
  15. let ReactDOMServer = require('react-dom/server');
    
  16. let Scheduler = require('scheduler');
    
  17. let act;
    
  18. let useEffect;
    
  19. let assertLog;
    
  20. let waitFor;
    
  21. let waitForAll;
    
  22. 
    
  23. describe('ReactDOMRoot', () => {
    
  24.   let container;
    
  25. 
    
  26.   beforeEach(() => {
    
  27.     jest.resetModules();
    
  28.     container = document.createElement('div');
    
  29.     React = require('react');
    
  30.     ReactDOM = require('react-dom');
    
  31.     ReactDOMClient = require('react-dom/client');
    
  32.     ReactDOMServer = require('react-dom/server');
    
  33.     Scheduler = require('scheduler');
    
  34.     act = require('internal-test-utils').act;
    
  35.     useEffect = React.useEffect;
    
  36. 
    
  37.     const InternalTestUtils = require('internal-test-utils');
    
  38.     assertLog = InternalTestUtils.assertLog;
    
  39.     waitFor = InternalTestUtils.waitFor;
    
  40.     waitForAll = InternalTestUtils.waitForAll;
    
  41.   });
    
  42. 
    
  43.   it('renders children', async () => {
    
  44.     const root = ReactDOMClient.createRoot(container);
    
  45.     root.render(<div>Hi</div>);
    
  46.     await waitForAll([]);
    
  47.     expect(container.textContent).toEqual('Hi');
    
  48.   });
    
  49. 
    
  50.   it('warns if you import createRoot from react-dom', async () => {
    
  51.     expect(() => ReactDOM.createRoot(container)).toErrorDev(
    
  52.       'You are importing createRoot from "react-dom" which is not supported. ' +
    
  53.         'You should instead import it from "react-dom/client".',
    
  54.       {
    
  55.         withoutStack: true,
    
  56.       },
    
  57.     );
    
  58.   });
    
  59. 
    
  60.   it('warns if you import hydrateRoot from react-dom', async () => {
    
  61.     expect(() => ReactDOM.hydrateRoot(container, null)).toErrorDev(
    
  62.       'You are importing hydrateRoot from "react-dom" which is not supported. ' +
    
  63.         'You should instead import it from "react-dom/client".',
    
  64.       {
    
  65.         withoutStack: true,
    
  66.       },
    
  67.     );
    
  68.   });
    
  69. 
    
  70.   it('warns if a callback parameter is provided to render', async () => {
    
  71.     const callback = jest.fn();
    
  72.     const root = ReactDOMClient.createRoot(container);
    
  73.     expect(() => root.render(<div>Hi</div>, callback)).toErrorDev(
    
  74.       'render(...): does not support the second callback argument. ' +
    
  75.         'To execute a side effect after rendering, declare it in a component body with useEffect().',
    
  76.       {withoutStack: true},
    
  77.     );
    
  78.     await waitForAll([]);
    
  79.     expect(callback).not.toHaveBeenCalled();
    
  80.   });
    
  81. 
    
  82.   it('warn if a container is passed to root.render(...)', async () => {
    
  83.     function App() {
    
  84.       return 'Child';
    
  85.     }
    
  86. 
    
  87.     const root = ReactDOMClient.createRoot(container);
    
  88.     expect(() => root.render(<App />, {})).toErrorDev(
    
  89.       'You passed a second argument to root.render(...) but it only accepts ' +
    
  90.         'one argument.',
    
  91.       {
    
  92.         withoutStack: true,
    
  93.       },
    
  94.     );
    
  95.   });
    
  96. 
    
  97.   it('warn if a container is passed to root.render(...)', async () => {
    
  98.     function App() {
    
  99.       return 'Child';
    
  100.     }
    
  101. 
    
  102.     const root = ReactDOMClient.createRoot(container);
    
  103.     expect(() => root.render(<App />, container)).toErrorDev(
    
  104.       'You passed a container to the second argument of root.render(...). ' +
    
  105.         "You don't need to pass it again since you already passed it to create " +
    
  106.         'the root.',
    
  107.       {
    
  108.         withoutStack: true,
    
  109.       },
    
  110.     );
    
  111.   });
    
  112. 
    
  113.   it('warns if a callback parameter is provided to unmount', async () => {
    
  114.     const callback = jest.fn();
    
  115.     const root = ReactDOMClient.createRoot(container);
    
  116.     root.render(<div>Hi</div>);
    
  117.     expect(() => root.unmount(callback)).toErrorDev(
    
  118.       'unmount(...): does not support a callback argument. ' +
    
  119.         'To execute a side effect after rendering, declare it in a component body with useEffect().',
    
  120.       {withoutStack: true},
    
  121.     );
    
  122.     await waitForAll([]);
    
  123.     expect(callback).not.toHaveBeenCalled();
    
  124.   });
    
  125. 
    
  126.   it('unmounts children', async () => {
    
  127.     const root = ReactDOMClient.createRoot(container);
    
  128.     root.render(<div>Hi</div>);
    
  129.     await waitForAll([]);
    
  130.     expect(container.textContent).toEqual('Hi');
    
  131.     root.unmount();
    
  132.     await waitForAll([]);
    
  133.     expect(container.textContent).toEqual('');
    
  134.   });
    
  135. 
    
  136.   it('supports hydration', async () => {
    
  137.     const markup = await new Promise(resolve =>
    
  138.       resolve(
    
  139.         ReactDOMServer.renderToString(
    
  140.           <div>
    
  141.             <span className="extra" />
    
  142.           </div>,
    
  143.         ),
    
  144.       ),
    
  145.     );
    
  146. 
    
  147.     // Does not hydrate by default
    
  148.     const container1 = document.createElement('div');
    
  149.     container1.innerHTML = markup;
    
  150.     const root1 = ReactDOMClient.createRoot(container1);
    
  151.     root1.render(
    
  152.       <div>
    
  153.         <span />
    
  154.       </div>,
    
  155.     );
    
  156.     await waitForAll([]);
    
  157. 
    
  158.     const container2 = document.createElement('div');
    
  159.     container2.innerHTML = markup;
    
  160.     ReactDOMClient.hydrateRoot(
    
  161.       container2,
    
  162.       <div>
    
  163.         <span />
    
  164.       </div>,
    
  165.     );
    
  166.     await expect(async () => await waitForAll([])).toErrorDev(
    
  167.       'Extra attributes',
    
  168.     );
    
  169.   });
    
  170. 
    
  171.   it('clears existing children with legacy API', async () => {
    
  172.     container.innerHTML = '<div>a</div><div>b</div>';
    
  173.     ReactDOM.render(
    
  174.       <div>
    
  175.         <span>c</span>
    
  176.         <span>d</span>
    
  177.       </div>,
    
  178.       container,
    
  179.     );
    
  180.     expect(container.textContent).toEqual('cd');
    
  181.     ReactDOM.render(
    
  182.       <div>
    
  183.         <span>d</span>
    
  184.         <span>c</span>
    
  185.       </div>,
    
  186.       container,
    
  187.     );
    
  188.     await waitForAll([]);
    
  189.     expect(container.textContent).toEqual('dc');
    
  190.   });
    
  191. 
    
  192.   it('clears existing children', async () => {
    
  193.     container.innerHTML = '<div>a</div><div>b</div>';
    
  194.     const root = ReactDOMClient.createRoot(container);
    
  195.     root.render(
    
  196.       <div>
    
  197.         <span>c</span>
    
  198.         <span>d</span>
    
  199.       </div>,
    
  200.     );
    
  201.     await waitForAll([]);
    
  202.     expect(container.textContent).toEqual('cd');
    
  203.     root.render(
    
  204.       <div>
    
  205.         <span>d</span>
    
  206.         <span>c</span>
    
  207.       </div>,
    
  208.     );
    
  209.     await waitForAll([]);
    
  210.     expect(container.textContent).toEqual('dc');
    
  211.   });
    
  212. 
    
  213.   it('throws a good message on invalid containers', () => {
    
  214.     expect(() => {
    
  215.       ReactDOMClient.createRoot(<div>Hi</div>);
    
  216.     }).toThrow('createRoot(...): Target container is not a DOM element.');
    
  217.   });
    
  218. 
    
  219.   it('warns when rendering with legacy API into createRoot() container', async () => {
    
  220.     const root = ReactDOMClient.createRoot(container);
    
  221.     root.render(<div>Hi</div>);
    
  222.     await waitForAll([]);
    
  223.     expect(container.textContent).toEqual('Hi');
    
  224.     expect(() => {
    
  225.       ReactDOM.render(<div>Bye</div>, container);
    
  226.     }).toErrorDev(
    
  227.       [
    
  228.         // We care about this warning:
    
  229.         'You are calling ReactDOM.render() on a container that was previously ' +
    
  230.           'passed to ReactDOMClient.createRoot(). This is not supported. ' +
    
  231.           'Did you mean to call root.render(element)?',
    
  232.         // This is more of a symptom but restructuring the code to avoid it isn't worth it:
    
  233.         'Replacing React-rendered children with a new root component.',
    
  234.       ],
    
  235.       {withoutStack: true},
    
  236.     );
    
  237.     await waitForAll([]);
    
  238.     // This works now but we could disallow it:
    
  239.     expect(container.textContent).toEqual('Bye');
    
  240.   });
    
  241. 
    
  242.   it('warns when hydrating with legacy API into createRoot() container', async () => {
    
  243.     const root = ReactDOMClient.createRoot(container);
    
  244.     root.render(<div>Hi</div>);
    
  245.     await waitForAll([]);
    
  246.     expect(container.textContent).toEqual('Hi');
    
  247.     expect(() => {
    
  248.       ReactDOM.hydrate(<div>Hi</div>, container);
    
  249.     }).toErrorDev(
    
  250.       [
    
  251.         // We care about this warning:
    
  252.         'You are calling ReactDOM.hydrate() on a container that was previously ' +
    
  253.           'passed to ReactDOMClient.createRoot(). This is not supported. ' +
    
  254.           'Did you mean to call hydrateRoot(container, element)?',
    
  255.         // This is more of a symptom but restructuring the code to avoid it isn't worth it:
    
  256.         'Replacing React-rendered children with a new root component.',
    
  257.       ],
    
  258.       {withoutStack: true},
    
  259.     );
    
  260.   });
    
  261. 
    
  262.   it('callback passed to legacy hydrate() API', () => {
    
  263.     container.innerHTML = '<div>Hi</div>';
    
  264.     ReactDOM.hydrate(<div>Hi</div>, container, () => {
    
  265.       Scheduler.log('callback');
    
  266.     });
    
  267.     expect(container.textContent).toEqual('Hi');
    
  268.     assertLog(['callback']);
    
  269.   });
    
  270. 
    
  271.   it('warns when unmounting with legacy API (no previous content)', async () => {
    
  272.     const root = ReactDOMClient.createRoot(container);
    
  273.     root.render(<div>Hi</div>);
    
  274.     await waitForAll([]);
    
  275.     expect(container.textContent).toEqual('Hi');
    
  276.     let unmounted = false;
    
  277.     expect(() => {
    
  278.       unmounted = ReactDOM.unmountComponentAtNode(container);
    
  279.     }).toErrorDev(
    
  280.       [
    
  281.         // We care about this warning:
    
  282.         'You are calling ReactDOM.unmountComponentAtNode() on a container that was previously ' +
    
  283.           'passed to ReactDOMClient.createRoot(). This is not supported. Did you mean to call root.unmount()?',
    
  284.         // This is more of a symptom but restructuring the code to avoid it isn't worth it:
    
  285.         "The node you're attempting to unmount was rendered by React and is not a top-level container.",
    
  286.       ],
    
  287.       {withoutStack: true},
    
  288.     );
    
  289.     expect(unmounted).toBe(false);
    
  290.     await waitForAll([]);
    
  291.     expect(container.textContent).toEqual('Hi');
    
  292.     root.unmount();
    
  293.     await waitForAll([]);
    
  294.     expect(container.textContent).toEqual('');
    
  295.   });
    
  296. 
    
  297.   it('warns when unmounting with legacy API (has previous content)', async () => {
    
  298.     // Currently createRoot().render() doesn't clear this.
    
  299.     container.appendChild(document.createElement('div'));
    
  300.     // The rest is the same as test above.
    
  301.     const root = ReactDOMClient.createRoot(container);
    
  302.     root.render(<div>Hi</div>);
    
  303.     await waitForAll([]);
    
  304.     expect(container.textContent).toEqual('Hi');
    
  305.     let unmounted = false;
    
  306.     expect(() => {
    
  307.       unmounted = ReactDOM.unmountComponentAtNode(container);
    
  308.     }).toErrorDev(
    
  309.       [
    
  310.         'Did you mean to call root.unmount()?',
    
  311.         // This is more of a symptom but restructuring the code to avoid it isn't worth it:
    
  312.         "The node you're attempting to unmount was rendered by React and is not a top-level container.",
    
  313.       ],
    
  314.       {withoutStack: true},
    
  315.     );
    
  316.     expect(unmounted).toBe(false);
    
  317.     await waitForAll([]);
    
  318.     expect(container.textContent).toEqual('Hi');
    
  319.     root.unmount();
    
  320.     await waitForAll([]);
    
  321.     expect(container.textContent).toEqual('');
    
  322.   });
    
  323. 
    
  324.   it('warns when passing legacy container to createRoot()', () => {
    
  325.     ReactDOM.render(<div>Hi</div>, container);
    
  326.     expect(() => {
    
  327.       ReactDOMClient.createRoot(container);
    
  328.     }).toErrorDev(
    
  329.       'You are calling ReactDOMClient.createRoot() on a container that was previously ' +
    
  330.         'passed to ReactDOM.render(). This is not supported.',
    
  331.       {withoutStack: true},
    
  332.     );
    
  333.   });
    
  334. 
    
  335.   it('warns when creating two roots managing the same container', () => {
    
  336.     ReactDOMClient.createRoot(container);
    
  337.     expect(() => {
    
  338.       ReactDOMClient.createRoot(container);
    
  339.     }).toErrorDev(
    
  340.       'You are calling ReactDOMClient.createRoot() on a container that ' +
    
  341.         'has already been passed to createRoot() before. Instead, call ' +
    
  342.         'root.render() on the existing root instead if you want to update it.',
    
  343.       {withoutStack: true},
    
  344.     );
    
  345.   });
    
  346. 
    
  347.   it('does not warn when creating second root after first one is unmounted', async () => {
    
  348.     const root = ReactDOMClient.createRoot(container);
    
  349.     root.unmount();
    
  350.     await waitForAll([]);
    
  351.     ReactDOMClient.createRoot(container); // No warning
    
  352.   });
    
  353. 
    
  354.   it('warns if creating a root on the document.body', async () => {
    
  355.     if (gate(flags => flags.enableFloat)) {
    
  356.       // we no longer expect an error for this if float is enabled
    
  357.       ReactDOMClient.createRoot(document.body);
    
  358.     } else {
    
  359.       expect(() => {
    
  360.         ReactDOMClient.createRoot(document.body);
    
  361.       }).toErrorDev(
    
  362.         'createRoot(): Creating roots directly with document.body is ' +
    
  363.           'discouraged, since its children are often manipulated by third-party ' +
    
  364.           'scripts and browser extensions. This may lead to subtle ' +
    
  365.           'reconciliation issues. Try using a container element created ' +
    
  366.           'for your app.',
    
  367.         {withoutStack: true},
    
  368.       );
    
  369.     }
    
  370.   });
    
  371. 
    
  372.   it('warns if updating a root that has had its contents removed', async () => {
    
  373.     const root = ReactDOMClient.createRoot(container);
    
  374.     root.render(<div>Hi</div>);
    
  375.     await waitForAll([]);
    
  376.     container.innerHTML = '';
    
  377. 
    
  378.     if (gate(flags => flags.enableFloat || flags.enableHostSingletons)) {
    
  379.       // When either of these flags are on this validation is turned off so we
    
  380.       // expect there to be no warnings
    
  381.       root.render(<div>Hi</div>);
    
  382.     } else {
    
  383.       expect(() => {
    
  384.         root.render(<div>Hi</div>);
    
  385.       }).toErrorDev(
    
  386.         'render(...): It looks like the React-rendered content of the ' +
    
  387.           'root container was removed without using React. This is not ' +
    
  388.           'supported and will cause errors. Instead, call ' +
    
  389.           "root.unmount() to empty a root's container.",
    
  390.         {withoutStack: true},
    
  391.       );
    
  392.     }
    
  393.   });
    
  394. 
    
  395.   it('opts-in to concurrent default updates', async () => {
    
  396.     const root = ReactDOMClient.createRoot(container, {
    
  397.       unstable_concurrentUpdatesByDefault: true,
    
  398.     });
    
  399. 
    
  400.     function Foo({value}) {
    
  401.       Scheduler.log(value);
    
  402.       return <div>{value}</div>;
    
  403.     }
    
  404. 
    
  405.     await act(() => {
    
  406.       root.render(<Foo value="a" />);
    
  407.     });
    
  408. 
    
  409.     expect(container.textContent).toEqual('a');
    
  410. 
    
  411.     await act(async () => {
    
  412.       root.render(<Foo value="b" />);
    
  413. 
    
  414.       assertLog(['a']);
    
  415.       expect(container.textContent).toEqual('a');
    
  416. 
    
  417.       await waitFor(['b']);
    
  418.       if (gate(flags => flags.allowConcurrentByDefault)) {
    
  419.         expect(container.textContent).toEqual('a');
    
  420.       } else {
    
  421.         expect(container.textContent).toEqual('b');
    
  422.       }
    
  423.     });
    
  424.     expect(container.textContent).toEqual('b');
    
  425.   });
    
  426. 
    
  427.   it('unmount is synchronous', async () => {
    
  428.     const root = ReactDOMClient.createRoot(container);
    
  429.     await act(() => {
    
  430.       root.render('Hi');
    
  431.     });
    
  432.     expect(container.textContent).toEqual('Hi');
    
  433. 
    
  434.     await act(() => {
    
  435.       root.unmount();
    
  436.       // Should have already unmounted
    
  437.       expect(container.textContent).toEqual('');
    
  438.     });
    
  439.   });
    
  440. 
    
  441.   it('throws if an unmounted root is updated', async () => {
    
  442.     const root = ReactDOMClient.createRoot(container);
    
  443.     await act(() => {
    
  444.       root.render('Hi');
    
  445.     });
    
  446.     expect(container.textContent).toEqual('Hi');
    
  447. 
    
  448.     root.unmount();
    
  449. 
    
  450.     expect(() => root.render("I'm back")).toThrow(
    
  451.       'Cannot update an unmounted root.',
    
  452.     );
    
  453.   });
    
  454. 
    
  455.   it('warns if root is unmounted inside an effect', async () => {
    
  456.     const container1 = document.createElement('div');
    
  457.     const root1 = ReactDOMClient.createRoot(container1);
    
  458.     const container2 = document.createElement('div');
    
  459.     const root2 = ReactDOMClient.createRoot(container2);
    
  460. 
    
  461.     function App({step}) {
    
  462.       useEffect(() => {
    
  463.         if (step === 2) {
    
  464.           root2.unmount();
    
  465.         }
    
  466.       }, [step]);
    
  467.       return 'Hi';
    
  468.     }
    
  469. 
    
  470.     await act(() => {
    
  471.       root1.render(<App step={1} />);
    
  472.     });
    
  473.     expect(container1.textContent).toEqual('Hi');
    
  474. 
    
  475.     expect(() => {
    
  476.       ReactDOM.flushSync(() => {
    
  477.         root1.render(<App step={2} />);
    
  478.       });
    
  479.     }).toErrorDev(
    
  480.       'Attempted to synchronously unmount a root while React was ' +
    
  481.         'already rendering.',
    
  482.     );
    
  483.   });
    
  484. 
    
  485.   // @gate disableCommentsAsDOMContainers
    
  486.   it('errors if container is a comment node', () => {
    
  487.     // This is an old feature used by www. Disabled in the open source build.
    
  488.     const div = document.createElement('div');
    
  489.     div.innerHTML = '<!-- react-mount-point-unstable -->';
    
  490.     const commentNode = div.childNodes[0];
    
  491. 
    
  492.     expect(() => ReactDOMClient.createRoot(commentNode)).toThrow(
    
  493.       'createRoot(...): Target container is not a DOM element.',
    
  494.     );
    
  495.     expect(() => ReactDOMClient.hydrateRoot(commentNode)).toThrow(
    
  496.       'hydrateRoot(...): Target container is not a DOM element.',
    
  497.     );
    
  498. 
    
  499.     // Still works in the legacy API
    
  500.     ReactDOM.render(<div />, commentNode);
    
  501.   });
    
  502. 
    
  503.   it('warn if no children passed to hydrateRoot', async () => {
    
  504.     expect(() => ReactDOMClient.hydrateRoot(container)).toErrorDev(
    
  505.       'Must provide initial children as second argument to hydrateRoot.',
    
  506.       {withoutStack: true},
    
  507.     );
    
  508.   });
    
  509. 
    
  510.   it('warn if JSX passed to createRoot', async () => {
    
  511.     function App() {
    
  512.       return 'Child';
    
  513.     }
    
  514. 
    
  515.     expect(() => ReactDOMClient.createRoot(container, <App />)).toErrorDev(
    
  516.       'You passed a JSX element to createRoot. You probably meant to call ' +
    
  517.         'root.render instead',
    
  518.       {
    
  519.         withoutStack: true,
    
  520.       },
    
  521.     );
    
  522.   });
    
  523. });