React logo on a dark background

React 18 evolves the popular JavaScript component framework with new features built around concurrent rendering and suspense. It promises better performance, more capabilities, and an improved developer experience for apps that make the switch.

In this article, we’ll show you how to upgrade your existing codebases to React 18. Bear in mind that this guide is only an overview of the most broadly applicable changes. Migration should be fairly painless for small projects already following React best practices; large sets of complex components may throw up some issues, which we’ll detail below.

Installing React 18

Before doing anything else, use npm to upgrade your project’s React dependency to v18:

$ npm install react@latest react-dom@latest

The new release doesn’t technically have any backwards incompatibilities. The new features are activated on an opt-in basis. As you’ve not changed any code yet, you should be able to start your app and observe it rendering correctly. Your project will run with its existing React 17 behavior.

$ npm start

Enabling React 18 Features: The New Root API

Using React 18 without any codebase changes will cause one side effect: you’ll see a browser console warning each time your app mounts in development mode.

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.

This deprecation message can be safely ignored if you’re not ready to upgrade your project. When you want to adopt React 18 capabilities, you need to make the change it describes. The old ReactDOM.render() function has been replaced with a new root API that’s more object-oriented. Besides improved ease-of-use, it also activates the concurrent rendering system that powers all the new headline features.

Within your index.js or app.js file, look for the lines that are similar to these:

import App from "./App.js";
import ReactDOM from "react-dom";
 
const container = document.getElementById("react");
ReactDOM.render(<App />, container);

This is a typical entrypoint for a React application. It renders an instance of the imported App component as your app’s root element. The rendered content is deposited as the innerHTML of the HTML element with id="react".

To switch to the React 18 root API, replace the code above with the following:

import App from "./App.js";
import {createRoot} from "react-dom/client";
 
const container = document.getElementById("react");
const root = createRoot(container);
root.render(<App />);

This has an equivalent effect to the old ReactDOM.render() API. Instead of initializing a root element and rendering your app as a single imperative operation, React 18 makes you create a root object first and then explicitly render your content.

Next look for any places in your code where you unmount your root node. Change ReactDOM.unmountComponentAtNode() to the new unmount() method on your root object:

// Before
import App from "./App.js";
import ReactDOM from "react-dom";
 
const container = document.getElementById("react");
ReactDOM.render(<App />, container);
ReactDOM.unmountComponentAtNode(container);
 
// After
import App from "./App.js";
import {createRoot} from "react-dom/client";
 
const container = document.getElementById("react");
const root = createRoot(container);
root.render(<App />);
root.unmount();

Replacing Render Callbacks

The ReactDOM.render() method’s optional callback argument has no direct counterpart in the React 18 root API. You could previously use this code to log Rendered! to the console after React has finished rendering the root node:

import App from "./App.js";
import ReactDOM from "react-dom";
 
const container = document.getElementById("react");
ReactDOM.render(<App />, container, () => console.log("Rendered!"));

This functionality was removed because the timing of the callback invocation is unpredictable when using React 18’s new partial hydration and streaming server rendering features. If you’re already using render callbacks and need to maintain compatibility, you can achieve similar behavior using the refs mechanism:

import {createRoot} from "react-dom/client";
 
const App = ({callback}) => (
    <div ref={callback}>
        <h1>Demo App</h1>
    </div>
);
 
const container = document.getElementById("react");
const root = createRoot(container);
root.render(<App callback={() => console.log("Rendered!")} />);

React calls function refs when components mount. Setting a ref on the component that’s your root node lets you detect when rendering occurs, providing a similar effect to the old render callback system.

Debugging Upgrade Problems

Your app should now be rendering using React 18 features and without any console warnings. Test your app thoroughly to make sure everything still works as you expect. If you find problems, you might be able to resolve them with these common resolutions.

Check for <StrictMode>

Apps wrapped in the <StrictMode> component may behave differently when rendering in React 18’s development mode. This is because Strict Mode now tests whether your codebase supports reusable state, a concept that will be fully introduced to React in a future release.

Reusable state allows React to remount a previously removed component with its last state automatically restored. This requires your components be resilient to double invocation of effects. Strict Mode now helps you prepare for reusable state by simulating mounting, unmounting, and remounting your components each time they’re used, surfacing any problems where the previous state can’t be restored. You can disable Strict Mode if it finds issues in your app or its dependencies that you’re not ready to address.

Support State Update Batching

React 18 changes how state updates are “batched” to improve performance. When you change state values multiple times in a function, React tries to combine them into a single re-render:

const Component = () => {
 
    const [query, setQuery] = useState("");
    const [queryCount, setQueryCount] = useState(0);
 
    /**
     * Two state updates, only one re-render
     */
    setQuery("demo");
    setQueryCount(queryCount + 1);
 
};

This mechanism increases efficiency but previously only worked inside React event handlers. With React 18, it works with all state updates, even if they originate from native event handlers, timeouts, or Promises. Some code might behave differently to before if you make consecutive state updates in any of these places.

const Component = () => {
 
    const [query, setQuery] = useState("");
    const [queryId, setQueryId] = useState("");
    const [queryCount, setQueryCount] = useState(0);
 
    const handleSearch = query => {
       fetch(query).then(() => {
 
            setQuery("demo");
            setQueryCount(1);
 
            // In React 17, sets to "query-1"
            // In React 18, sets to "query-0" - previous state update is batched with this one
            setQueryId(`query-${queryCount}`);
 
        });
    }
 
};

You can disable this behavior in situations where you’re not ready to refactor your code. Wrap state updates in flushSync() to force them to commit immediately:

const Component = () => {
 
    const [query, setQuery] = useState("");
    const [queryId, setQueryId] = useState("");
    const [queryCount, setQueryCount] = useState(0);
 
    const handleSearch = query => {
       fetch(query).then(() => {
 
            flushSync(() => {
                setQuery("demo");
                setQueryCount(1);
            });
 
            // Sets to "query-1"
            setQueryId(`query-${queryCount}`);
 
        });
    }
 
};

Stop Using Removed and Unsupported Features

Once all the above aspects have been addressed, your app should be fully compatible with React 18. Although there are a few more API surface changes, these shouldn’t impact the majority of apps. Here are some to be aware of:

  • unstable_changedBits has been removed – This unsupported API allowed opting out of context updates. It is no longer available.
  • The Object.assign() polyfill has been removed – You should manually add the object-assign polyfill package if you need to support very old browsers without a built-in Object.assign().
  • Internet Explorer is no longer supported – React has officially dropped compatibility with Internet Explorer ahead of the browser’s end of support in June. You should not upgrade to React 18 if you still require your app to run in IE.
  • Using Suspense with an undefined fallback is now equivalent to null – Suspense boundaries with fallback={undefined} were previously skipped, allowing code to cascade to the next parent boundary in the tree. React 18 now respects Suspense components without a fallback.

Server Side Rendering

Apps that use server side rendering will require a few more changes to work with React 18.

Inline with the new root API, you must replace the old hydrate() function in your client-side code with the new hydrateRoot() provided by the react-dom/client package:

// Before
import App from "./App.js";
import ReactDOM from "react-dom";
 
const container = document.getElementById("react");
ReactDOM.hydrate(<App />, container);
 
// After
import App from "./App.js";
import {createRoot} from "react-dom/client";
 
const container = document.getElementById("react");
const root = hydrateRoot(container, <App />);

In your server side code, replace deprecated rendering API calls with their new counterparts. In most cases, you should change renderToNodeStream() to the new renderToReadableStream(). The new stream APIs unlock access to React 18’s streaming server rendering capabilities, where the server can keep delivering new HTML to the browser after your app’s initial render.

Start Using React 18 Features

Now that you’ve upgraded you can start making your app more powerful by incorporating React 18 features. React’s use of concurrency means component renders can be interrupted, unlocking new capabilities and more responsive UIs.

Some of the added features include major updates to Suspense, a way to designate the priority of state updates with Transitions, and a built-in mechanism for throttling re-renders caused by non-urgent but high-frequency updates. There are several miscellaneous changes and improvements too: you can return undefined from a component’s render() method, the warning about calling setState() on unmounted components has been removed, and several new HTML attributes such as imageSizes, imageSrcSet, and aria-description are recognized by React DOM’s renderer.

Summary

React 18 is stable and ready to use. In most cases the upgrade process should be quick and easy, requiring only an npm update and a switch to the new root API. You should still test all your components though: they may behave differently in some situations, such as in Strict Mode or when automatic batching applies.

This new release points to the future direction of React as a high-performance framework for all kinds of web applications. It also extends React’s server-side rendering capabilities, adding Suspense on the server and the ability to keep streaming content to your users after the initial render. This gives developers more flexibility to distribute rendering across both the client and server.

Profile Photo for James Walker James Walker
James Walker is a contributor to How-To Geek DevOps. He is the founder of Heron Web, a UK-based digital agency providing bespoke software development services to SMEs. He has experience managing complete end-to-end web development workflows, using technologies including Linux, GitLab, Docker, and Kubernetes.
Read Full Bio »