Upgrading to react@18 and react-dom@18 fails

Viewed 11515

I have an existing React 17 app that started life as a create-react-app output with the TypeScript option. I wanted to change package.json to include react@18.0.0 and react-dom@18.0.0 and then start the app, and that step worked fine.

Then I tried to change the index.tsx file the way that the React 18 migration instructions tell you to do it, by changing the relevant segment in index.tsx to:

ReactDOM.createRoot(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

and either npm run build or npm run start gives me compile errors, saying that createRoot is not defined. This is probably due to the fact that I have dependencies on @types/react@17.0.43 and @types/react-dom@17.0.14. There do not seem to be any updated versions of these files from DefinitelyTyped for React 18 yet.

So, I went back and created a brand new app with npx create-react-app react18 --template typescript. It now wants react@18 and react-dom@18, but still has the old @types/ dependencies. It also gives the same compile error when you switch from render() to createRoot().

As one further step, I asked myself "what happens if we leave out TypeScript?" Created yet another app with npx create-react-app react18js. It depends on react@18.0.0 and react-dom@18.0.0 as expected, but still generates ReactDOM.render() in its index.js output.

That works, but change ReactDOM.render to ReactDOM.createRoot and you get no compile errors, but you also get no visible output if you try npm run start. It doesn't seem to render the <App/> component at all.

Am I missing something? Are the instructions not quite adequate?

11 Answers

You need first to import ReactDOM from react-dom/client and then create the root element from the element id=root using createRoot and then use .render to render the content of the <App/> component inside the created root element like this:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

let root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);

root.render(
  <React.StrictMode>
      <App />
  </React.StrictMode>,
);

The React documentation explains how to use ReactDOM.createRoot, which is not a direct/drop-in replacement for ReactDOM.render.

What the doc doesn't cover is the TypeScript way of using it.

I know this solution is a bit overkill compared to others here that uses type assertion (as HTMLElement) on the DOM element, but it fully utilizes the benefit of strict typing (e.g. if someone accidentally removed the #root element in index.html)

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

const el = document.getElementById('root')
if (el === null) throw new Error('Root container missing in index.html')

const root = ReactDOM.createRoot(el)
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

Changing the import statement is the crucial first step in getting this to work, followed by the fact that the parameters for createRoot() are different. For the record, this is what you need to change your create-react-app src/index.tsx (or src/index.jsx if not using Typescript) to look like:

import React from 'react';
import ReactDOMClient from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOMClient.createRoot(document.getElementById("root") as HTMLElement);
root.render(
    <React.StrictMode>
        <App />
    </React.StrictMode>
);

Next step is to clean up tests, where the render() method from @testing-library/react still uses the old approach -- but that's a separate topic.

Try this code

import React from 'react';
import ReactDOM from "react-dom/client";

import App from './App';

const root = ReactDOM.createRoot(document.getElementById("root"));

root.render(
  <StrictMode>
    <App />
  </StrictMode>
);

In the meantime @types/react and @types/react-dom updates for React 18, we need to load the types manually.

To load the types declared in an actual project, there are three ways. The easiest one, if your tsconfig.json already has a "types" array in the "compilerOptions" section, is to add "react/next" to the "types" array.

Alternatively, a specific import syntax can to be used from a typescript file. This module does not exist in reality, which is why the {} is important:

import {} from 'react/next'

It is also possible to include it through a triple-slash reference:

/// <reference types="react/next" />

Either the import or the reference only needs to appear once, anywhere in the project.

How to Upgrade to React 18

  • React 18 introduces a new root API which provides better ergonomics for managing roots. The new root API also enables the new concurrent renderer, which allows you to opt-into concurrent features
  • Installing

To install the latest version of React:

npm install react react-dom

Or if you’re using yarn:

yarn add react react-dom
// Before
import { render } from 'react-dom';
const container = document.getElementById('app');
render( 
      <React.StrictMode>
        <App />
      </React.StrictMode>
     ,container);

// After
import { createRoot } from 'react-dom/client';
const container = document.getElementById('root') as HTMLElement;
const root = ReactDOM.createRoot(container); 

root.render(
  <React.StrictMode>
      <App />
  </React.StrictMode>,
);

React 18 update guide

The official reactjs blog explains how to convert your code to React 18, but it lacks instruction on how to update your DefinitelyTyped resources for TypeScript. I had to update the @types/react-dom package:

npm install @types/react-dom@17.0.14

This updated type definition will contain the updated declarations for React 18. Here's my updated index.tsx file:

import React from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';

// React 18        
const container = document.getElementById('root') as HTMLElement;
const root = createRoot(container);
root.render(
  <React.StrictMode>
      <App />
  </React.StrictMode>
);

React officially post on their blog How to Upgrade to React 18 but there have missing instructions.

First of all, you have to install the latest version of React:

npm install react react-dom

Or if you’re using yarn: (my personal opinion yarn is better)

yarn add react react-dom

then go to index.js remove old code and paste it.

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

import App from "./App";

const container = document.getElementById("root");
const root = createRoot(container);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

I met the same question with you. The reason is that react version in package.json is ^18, but file content for our use in node_modules/@types/react is for React V17.

try :

  1. delete node_modules/@types/react, node_modules/@types/react-dom two files
  2. npm i --save-dev @types/react@latest npm i --save-dev @types/react-dom@latest

It's useful for me :)

Because you are using typescript You need to upgrade @types/react @types/react-dom as well.

npm uninstall @types/react @types/react-dom


npm i -D @types/react @types/react-dom


Or you can update them directly to the latest version

Then update your index.js as follow:

import React from 'react';

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


const root = createRoot(document.getElementById('root') as HTMLDivElement);

root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

Also consider close and open your editor if it is needed

You have to define root element with createRoot and render the component.

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
Related