Home > Back-end >  Switching from ReactDOM.render to createRoot makes simple jest test fail
Switching from ReactDOM.render to createRoot makes simple jest test fail

Time:07-18

I just started studying "Mastering React Test-Driven Development" by Daniel Irvine, and I figured that it shouldn't be too hard to convert the examples to React 18. But I am running into trouble converting the very first test in the book using Jest.

The book doesn't use create-react-app or anything, but instead builds the React apps from scratch, so I'm having trouble finding relevant examples of how to convert the code.

When written as in the book, in React 17 style, the test passes. But if I replace ReactDOM.render() with createRoot(), the test fails.

My application directory looks like:

├── package.json
├── package-lock.json
├── src
│   └── Appointment.js
└── test
    └── Appointment.test.js

and the file contents are:

package.json:

{
  "name": "appointments",
  "version": "1.0.0",
  "description": "Appointments project from Mastering React Test-Driven Development.",
  "main": "index.js",
  "scripts": {
    "test": "jest"
  },
  "repository": {
    "type": "git",
    "url": "example.com"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/plugin-transform-runtime": "^7.18.6",
    "@babel/preset-env": "^7.18.6",
    "@babel/preset-react": "^7.18.6",
    "jest": "^28.1.2",
    "jest-environment-jsdom": "^28.1.3"
  },
  "dependencies": {
    "@babel/runtime": "^7.18.6",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "jest": {
    "testEnvironment": "jsdom"
  }
}

src/Appointment.js:

import React from 'react';

export const Appointment = () => <div>Ashley</div>;

test/Appointment.test.js:

import React from 'react';
import ReactDOM from 'react-dom';
// import {createRoot} from 'react-dom/client';

import {Appointment} from '../src/Appointment';

describe('Appointment', () => {
  it("renders the customer's first name.", () => {
    const customer = {firstName: 'Ashley'};
    const component = <Appointment customer={customer} />;
    const container = document.createElement('div');
    document.body.appendChild(container);

    ReactDOM.render(component, container);

    // const root = createRoot(container);
    // root.render(component);

    expect(document.body.textContent).toMatch('Ashley');
  });
});

With ReactDOM.render(), the test passes, but I get the following error:

  console.error
    Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot

      11 |     document.body.appendChild(container);
      12 |
    > 13 |     ReactDOM.render(component, container);
         |              ^
      14 |
      15 |     expect(document.body.textContent).toMatch('Ashley');
      16 |   });

      at printWarning (node_modules/react-dom/cjs/react-dom.development.js:86:30)
      at error (node_modules/react-dom/cjs/react-dom.development.js:60:7)
      at Object.render (node_modules/react-dom/cjs/react-dom.development.js:29670:5)
      at Object.render (test/Appointment.test.js:13:14)

I looked up how to convert ReactDOM.render() to createRoot(), and changed the test to:

import React from 'react';
// import ReactDOM from 'react-dom';
import {createRoot} from 'react-dom/client';

import {Appointment} from '../src/Appointment';

describe('Appointment', () => {
  it("renders the customer's first name.", () => {
    const customer = {firstName: 'Ashley'};
    const component = <Appointment customer={customer} />;
    const container = document.createElement('div');
    document.body.appendChild(container);

    // ReactDOM.render(component, container);

    const root = createRoot(container);
    root.render(component);

    expect(document.body.textContent).toMatch('Ashley');
  });
});

and the test fails as follows:


> [email protected] test
> jest

 FAIL  test/Appointment.test.js
  Appointment
    ✕ renders the customer's first name. (9 ms)

  ● Appointment › renders the customer's first name.

    expect(received).toMatch(expected)

    Expected substring: "Ashley"
    Received string:    ""

      17 |     root.render(component);
      18 |
    > 19 |     expect(document.body.textContent).toMatch('Ashley');
         |                                       ^
      20 |   });
      21 | });
      22 |

      at Object.toMatch (test/Appointment.test.js:19:39)
      at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:317:13)
      at runJest (node_modules/@jest/core/build/runJest.js:407:19)
      at _run10000 (node_modules/@jest/core/build/cli/index.js:339:7)
      at runCLI (node_modules/@jest/core/build/cli/index.js:190:3)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.979 s, estimated 1 s
Ran all test suites.

How can I get this test to pass using createRoot()?

CodePudding user response:

React 17 looks to render immediately when you call ReactDOM.render:

const App = () => {
    return 'foo';
};

ReactDOM.render(<App />, document.querySelector('.react'));
console.log(document.querySelector('.react').textContent);
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<div class='react'></div>

In contrast, React 18 doesn't, it performs the render work only once any other code has finished (which will usually be a few milliseconds later):

const App = () => {
    return 'foo';
};

ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
console.log(document.querySelector('.react').textContent.trim());
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div class='react'></div>

This difference is imperceptible to the eye, but will cause the test to fail because the rendering has not occurred by the time your expect runs.

One option is to use the callback form of the test so that you can call expect after rendering has occurred.

it("renders the customer's first name.", (done) => {
    const component = <Appointment customer={ { firstName: 'Ashley' } } />;
    const container = document.body.appendChild(document.createElement('div'));
    createRoot(container).render(component);
    setTimeout(() => {
        expect(document.body.textContent).toMatch('Ashley');
        done();
    });
});

const App = () => {
    return 'foo';
};

ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
setTimeout(() => {
    console.log(document.querySelector('.react').textContent.trim());
});
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<div class='react'></div>

  • Related