An Ultimate Guide to Upgrading to React 18
[React]((https://reactjs.org) is a free and open-source front-end JavaScript library for building user interface with the use of UI components. It makes it easier to build interactive UIs by enabling you to create reusable UI components that can be used to build the view layer of web and mobile apps. The React team recently released React 18 with improvements like automatic batching, a new suspense feature, and new APIs like startTransition
. New features in React 18 are based on its new concurrent renderer, with a gradual adoption strategy for existing applications. This article will briefly describe the new features in React 18 and a step-by-step process for upgrading to React 18.
The new React18 version includes out-of-the-box improvements such as Automatic Batching, new APIs like startTransition
, and streaming server-side rendering with support for Suspense. A lot of the new features in React 18 are built on top of a new concurrent renderer, a behind-the-scenes that enables powerful new functionality. Concurrent React is opt-in (it’s enabled when you use a concurrent feature) but it seems to be a game-changer in the way people build React applications.
Concurrent React
One of the major additions to React 18 is Concurrency. According to the React team, Concurrency is not a feature per se. It’s a new behind-the-scenes mechanism that enables React to simultaneously prepare multiple versions of your UI. As a result, React will delegate to the developer the task of identifying which changes are essential before re-rendering the component.
Before React 18, rendering was a single, uninterrupted, synchronous operation, and once it had begun, it could not be interrupted. Concurrency is an important update to React’s rendering mechanism. In conjunction with Concurrency, React allows us to interrupt rendering.
The concurrent rendering in React 18 is a powerful tool, and most of the new features such as Suspense, transition, and streaming server are built on it. You can learn more about concurrent React here.
New Client and Server Rendering APIs
With this latest release, the React team redesigned the API expose for rendering on the client and server. With these changes, you can continue using the old API in React 17 mode while upgrading to the new APIs in React 18.
These new APIs below, are exported from react-dom/client
. We will see how to integrate them in the next section on upgrading to React 18. The new features in React 18 won’t work without it. You can also learn more about React DOM Client here.
hydrateRoot
, to hydrate a server-rendered application. You should use it instead ofReactDOM.hydrate
in conjunction with the new React DOM Server APIs.createRoot
, to creating a root torender
orunmount
. This should be used instead ofReactDOM.render
.
The following new APIs are exported from react-dom/server
, and provide full support for streaming Suspense on the server. You can also learn more about React DOM SERVER here.
renderToPipeableStream
, used for streaming in a Node environmentrenderToReadableStream
, used for modern edge runtime environments, such as Cloudflare workers and Deno.
Strict Mode
In React 18, the strict mode simulates mounting, unmounting, and remounting the component with a previous state. For instance, when a user tabs away from a screen and back, React should be able to display the previous screen quickly. To accomplish this, React would unmount and remount trees using the previous component state as before. This feature improves React apps’ performance but requires components are resilient to the effect of being mounted and unmounted multiple times.
Automatic Batching
Batching is when React merges multiple state updates into a single re-render for improved performance. For instance, if you have two state updates inside the same click event, React has always batched these into a single update and re-render. This technique is considerably great for performance because it avoids unnecessary re-renders and prevents components from rendering with an incomplete state where only one state variable was updated, which may lead to irrelevant bugs. If you run the following code below, you will notice that on every click, React only performs a single render; meanwhile, we set the state twice.
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount((c) => c + 1); // Does not re-render yet
setFlag((f) => !f); // Does not re-render yet
// React will only re-render once at the end (that's batching!)
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "green" : "red" }}>{count}</h1>
</div>
);
}
Before introducing React 18, you can only batch updates inside React event handlers. Any updates inside setTimeout
, promises, native event handlers, or any other event were batched in React by default. With createRoot
, React 18 will batch together all state updates, irrespective of where they originated. As a result, any update within promises, timeouts, native event handlers, or any other event will batch just like updates within React events.
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
fetchSomething().then(() => {
// React 18 and later versions does batch these.
setCount((c) => c + 1);
setFlag((f) => !f);
// React will rerender once at the end (that's batching!)
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "green" : "red" }}>{count}</h1>
</div>
);
}
Occasionally, you might need to re-render a component or read something from the DOM immediately after a state change. You can use ReactDOM.flushSync()
to opt out of batching in such a situation.
import { flushSync } from "react-dom"; // Note: react-dom, not react
function handleClick() {
flushSync(() => {
setCounter((c) => c + 1);
});
// React has updated the DOM by now.
flushSync(() => {
setFlag((f) => !f);
});
// React has updated the DOM by now.
}
Transitions
Transitions are a new concept introduced in React 18 to distinguish between urgent and non-urgent updates. As the name implies, urgent updates reflect direct user interaction, such as clicking, pressing, typing, etc., while transition updates (non-urgent updates) switch the UI from one view to another.
With this new feature, developers can now split updates into urgent and low-priority categories. For instance, when typing into search input, you might want to show feedback on the content being typed to the user and search functionality that runs in the background that searches for the input data. Providing visual feedback to users is important and urgent, but the searching functionality is not so urgent and can be marked as non-urgent.
Updates that are not urgent are called transitions. When you mark non-urgent UI updates as “transition”, React will decide which updates to prioritize. By doing so, rendering can be optimized, and stale rendering can be eliminated. You can wrap updates as non-urgent with startTransition
, which will be interrupted if there is a more urgent update. You can learn more about transitions in React 18 here.
import { startTransition } from "react";
// Urgent: Show what was typed
setInputValue(input);
// Mark any state updates inside as transitions
startTransition(() => {
// Transition: Show the results
setSearchQuery(input);
});
Suspense
In React 18, you can now use Suspense
on the server. With it, you can specify the loading state for a part of the component tree if it’s not yet ready for display. According to the React team, Suspense
makes the “UI loading state” a first-class declarative concept in React, enabling you to build higher-level features. When a server response is delayed, React can stream HTML for the fallback so that the rest of the page can be viewed. Upon completion of the data, the user will be able to view the HTML content. Therefore, the entire page will no longer be slowed down by a slow data source on the server. To learn more about Suspense, check here.
Open Source Session Replay
OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.
Start enjoying your debugging experience - start using OpenReplay for free.
How to Upgrade to React 18
You can install the latest version of React 18 and React DOM with either npm/yarn
like this below:
npm install react react-dom
or, if you’re using yarn:
yarn add react react-dom
Updating to Client Rendering APIs
After installing React 18, you’ll see a warning message in the console like this:
ReactDOM.render is no longer supported in React 18. Use `createRoot` instead. Until you do this switch, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot.
In React 18, the Root API provides a more intuitive way to manage roots. Additionally, the new root API enables the new concurrent renderer, so you can opt into using concurrent features. After that, you’ll want to use createRooot
instead of render
. To do that, update ReactDom.render
to ReactDom.createRoot
to create a root, and render your app using root.
Previously in React 17, this is what it looked like:
import ReactDOM from "react-dom";
import App from "App";
const container = document.getElementById("app");
ReactDOM.render(<App />, container);
Now with React 18, this is what it looks like below:
import ReactDOM from "react-dom";
import App from "App";
const container = document.getElementbyId("app");
// create a root
const root = ReactDOM.createRoot(container);
// render app to root
root.render(<App />);
React also changed unmountComponentAtNode
to root.unmount
like this below:
// Before
unmountComponentAtNode(container);
// After
root.unmount();
In addition, if your application uses server-side rendering with hydration, you will need to upgrade hydrate
to hydrateRoot
:
import { hydrate } from "react-dom";
const container = document.getElementById("app");
hydrate(<App />, container);
// After
import { hydrateRoot } from "react-dom/client";
const container = document.getElementById("app");
const root = hydrateRoot(container, <App />);
// Unlike with createRoot, you don't need a separate root.render() call here.
To learn more, check the working group discussion here.
Note: Perhaps if your app doesn’t work after upgrading, check whether it’s wrapped in <strictMode>
. In React 18, the strict mode has gotten stricter, and not all your component may adapt to the new checks it adds in the development mode.
Updates to TypeScript Definition
If your project uses TypeScript, update your @type/react
and @types/react-dom
dependencies to the latest versions. Using the new types is safer, as it catches issues that the type checker previously ignored. One of the most significant changes is that the children
prop needs to be explicitly listed when defining props, for example:
interface MyButtonProps {
color: string;
children?: React.ReactNode;
}
You can check the React 18 typings pull request for a complete list of type-related changes.
How to configure your test environment
This warning below may appear in your console when you first update your tests to use createRoot
:
The current testing environment is not configured to support act(…)
To fix it, set globalThis.IS_REACT_ACT_ENVIRONMENT
to true before running your test:
// In your test setup file
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
By setting the flag, React will know that it’s running in a unit test-like environment. When you forget to wrap an update with act, React will log helpful warnings.
Since React 18, IE11 is no longer supported. If you need to continue supporting IE11, you’ll have to stay with React 17.
Conclusion
React 18 comes with several amazing features and concepts that improve the user experience. We have briefly explored some concepts like Concurrent React, New Client and Server Rendering APIs, Strict Mode, and some of its new features such as Automatic Batching, startTransition API, and a new suspense feature. We also looked at how to upgrade to React 18. Upgrading to React 18 should not cause any broken code and should be as straightforward as possible.