-- online Search!

Updating to Newer React Versions

by kirupa   |   filed under Learning React

Cool new features and under-the-hood improvements get added to React all the time. If your apps rely on React, you will find yourself at some point needing to move to a more recent version to take advantage of these changes. Sometimes, that is as easy as changing a number. Other times, an older app will need some updates before it works with the newer version of React.

In this tutorial, we'll look at how to upgrade an existing app using npm, what you should look out for when moving to React 18 and React 19, and how tools called codemods can do some of the tedious work for you.

Onwards!

Finding Out What You Have

Before you change anything, you should know what version of React your app is using. Most React apps these days are created with a tool like Vite and have a package.json file in the root of the project. Open it up and look for the react and react-dom entries:

{
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

The number in package.json is a range and not necessarily the exact version you have installed. To see what is actually installed, run the following command from your project folder:

npm ls react react-dom

You will get a list showing the exact versions of both packages. If you want to know whether newer versions are available, you can ask npm:

npm outdated

This prints a table with a Current column (what you have), a Wanted column (the newest version allowed by the range in your package.json), and a Latest column (the newest version available). If Latest is a higher major version than Wanted, you are looking at an upgrade that could include breaking changes.

Upgrading with npm

The actual upgrade is a single command. To move to the latest version of React, run this in your project folder:

npm install react@latest react-dom@latest

When it finishes, check your package.json file. Both entries will have been updated to the newest version (with a ^ in front of it), and your lock file will have been updated as well:

{
  "dependencies": {
    "react": "^19.3.1",
    "react-dom": "^19.3.1"
  }
}

If you would like to land on a particular major version instead, replace latest with the version you want. For example, this moves you to React 18:

npm install react@18 react-dom@18

Notice that we are always installing react and react-dom together. Whatever you do, make sure both packages end up on the same version. If you mix and match, you might run into some weird issues.

If you would rather have npm record the exact version without the ^, add the --save-exact flag. The ^ and ~ characters are how package.json says what updates are allowed when you run npm install later, and the official semver documentation goes into the details.

What about Create React App? If your project was created with Create React App, the npm install commands above work the same way. Keep in mind that Create React App is no longer actively maintained, and the React team now recommends starting new projects with a tool like Vite or a framework. Moving an older project over to one of those is a good idea, but it is a separate task from upgrading React itself.

The command is the easy part. The more interesting question is what your app does after you run it. That's where the rest of this tutorial comes in.

Upgrade One Major Version at a Time

If you are on React 16 or 17, I recommend that you don't jump straight to React 19. Take it one step at a time. First, upgrade to React 18, fix whatever breaks, and make sure your app works. Then upgrade to React 19. This way, when something goes wrong, you know which upgrade caused it.

If you are on React 18 already, there is a handy intermediate step. React 18.3 is identical to 18.2 except that it prints warnings in your console for the things that were removed or changed in React 19. You can install it, run your app, and then fix every warning before you upgrade:

npm install react@18.3 react-dom@18.3

Before any of these steps, make sure your project is committed to Git (or you have some other way to undo your changes). We are going to be making a lot of changes, and you want an easy way to go back.

Upgrading to React 18

React 18 had one change that almost every app needs to deal with. This is the way your app starts up.

From ReactDOM.render to createRoot

In React 17 and earlier, you started your app by calling ReactDOM.render:

import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(<App />, document.getElementById("root"));

React 18 replaced this with createRoot, which you import from react-dom/client:

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

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

In React 18, the old ReactDOM.render still works, but it prints a warning and runs your app in the older React 17 mode. You won't get any of the React 18 improvements until you switch to createRoot. In React 19, ReactDOM.render is gone altogether, so this is a change you can't skip.

A few related APIs changed as well:

  • ReactDOM.hydrate became hydrateRoot (this only matters if you render on the server)
  • ReactDOM.unmountComponentAtNode(container) became root.unmount()
  • The optional callback that you could pass as the third argument to ReactDOM.render was removed. If you need to run code after the first render, use an effect or a ref callback inside your component.

Automatic Batching

Once you are using createRoot, React batches state updates everywhere, including inside promises, timeouts, and native event handlers. Before, only updates inside React event handlers were batched. In other words, if you call two state setters in a row inside a setTimeout, you now get one render instead of two. This is almost always a good thing. If you have some code that depended on the DOM being updated between two state changes, you can wrap the first one in flushSync to opt out.

StrictMode Does More in Development

This is the one that surprises people the most. If you wrap your app with <StrictMode> (and the Vite template does this for you), then in development React 18 will mount your component, unmount it, and mount it again right away. It does this to make sure your effects can handle being cleaned up and run again, which is something that can happen for real in future React features.

If you have an effect that doesn't clean up after itself, you will see it run twice. Here is the kind of code that gets caught:

useEffect(() => {
  const id = setInterval(() => console.log("tick"), 1000);

  return () => clearInterval(id);
}, []);

The cleanup function is what makes this safe. Without the return line, you would end up with two timers running after the remount. This only happens during development. Your production build isn't affected. If your app starts misbehaving right after you upgrade to React 18, StrictMode is the first place I would look. It is almost always pointing out a real bug that was there all along.

Other Things to Know

  • Internet Explorer is no longer supported. React 18 relies on features that IE doesn't have.
  • Server rendering APIs changed. renderToNodeStream is deprecated in favor of renderToPipeableStream. If you don't render on the server, you can ignore this.
  • TypeScript users need to update their types. In @types/react 18, the children prop is no longer included automatically. If a component accepts children, you need to declare it in its props.
  • Test setups may need a tweak. If you see warnings about act in your tests, set globalThis.IS_REACT_ACT_ENVIRONMENT = true in your test setup.

React 18 also added new hooks like useId, useSyncExternalStore, useDeferredValue, and startTransition. You don't need any of them to upgrade, but they are there when you are ready.

Upgrading to React 19

React 19 is a bigger cleanup. Many APIs that had been deprecated for years were finally removed. If your app is already on 18.3 and isn't printing any warnings, you have very little to do. If you are coming from an older codebase, here is what is gone:

  • ReactDOM.render, ReactDOM.hydrate, ReactDOM.unmountComponentAtNode, and ReactDOM.findDOMNode. Use createRoot, hydrateRoot, root.unmount(), and refs instead. If you try to call one of the removed functions, your app will throw an error, because the function simply doesn't exist anymore.
  • propTypes and defaultProps on function components. React no longer checks propTypes, and defaultProps on function components is ignored. Use TypeScript for type checking and default parameters for default values.
  • String refs. Things like ref="input" are gone. Use useRef (or createRef in a class) instead.
  • Legacy context. The contextTypes and getChildContext approach is gone in favor of the createContext API we looked at earlier.
  • React.createFactory and module-pattern function components. Use JSX.
  • react-dom/test-utils. The act function now lives in the react package, so you would import it as import { act } from "react".
  • UMD builds. React no longer ships files you can load directly with a script tag. If you were doing that, you will want to use a build tool or load React as an ES module from a CDN.

For the first two, here is what the change looks like for a function component that used defaultProps:

// Before
function Greeting({ name }) {
  return <p>Hello, {name}!</p>;
}
Greeting.defaultProps = { name: "friend" };

// After
function Greeting({ name = "friend" }) {
  return <p>Hello, {name}!</p>;
}

A couple of other changes won't break your app outright, but you should know about them:

  • The new JSX transform is required. Any modern setup, including Vite, already uses it. Very old setups that still do import React from "react" just to make JSX work should be checked.
  • Errors are reported differently. Errors that happen while rendering are no longer re-thrown a second time. Uncaught errors go to window.reportError, and caught ones go to console.error. If you want to handle them yourself, createRoot now accepts onUncaughtError and onCaughtError options.
  • Accessing a ref on an element. Reading element.ref is deprecated. Use element.props.ref instead. Also, function components can now receive ref as a regular prop, so forwardRef is no longer needed for new code.
  • StrictMode is a little smarter. When it renders your components twice in development, useMemo and useCallback now reuse the results from the first render instead of calculating them twice. Ref callbacks are also called twice on mount, so make sure they are okay with being cleaned up and set again.
  • Suspense behaves a little differently. When a component suspends, React now shows the fallback right away instead of waiting for its siblings to finish rendering.
  • TypeScript users have more to update. Upgrade @types/react and @types/react-dom to the 19 versions. Also, useRef now requires an argument, ref callbacks can't implicitly return a value, and the global JSX namespace moved to React.JSX.

If you want to read the official, line-by-line version of all of this, the React team maintains upgrade guides for React 18 and React 19.

Letting Codemods Do the Work

Making a lot of repetitive changes by hand isn't fun. This is where codemods come in. A codemod is a script that reads your code, finds the old pattern, and rewrites it into the new one. The React team provides a collection of them for these upgrades.

There is one important thing to do first. The codemod tool refuses to run when you have uncommitted changes in Git. This is on purpose, because it modifies your files. So commit (or stash) your work before you continue:

git add -A
git commit -m "Before running codemods"

To run the whole set of changes for React 19 in one go, run the migration recipe from your project folder:

npx codemod@latest react/19/migration-recipe

This recipe bundles together a handful of transformations: it replaces ReactDOM.render with createRoot, replaces string refs, moves the act import to the react package, renames useFormState to useActionState, and converts propTypes to TypeScript types. If you would rather run them one at a time so that you can review each change, you can run the individual codemods:

npx codemod@latest react/19/replace-reactdom-render
npx codemod@latest react/19/replace-string-ref
npx codemod@latest react/19/replace-act-import

If you are using TypeScript, there is a separate tool for updating your types:

npx types-react-codemod@latest preset-19 ./src

There is also the older react-codemod package that you can run with npx react-codemod <transform> <path>. It's the same collection of transforms, just run through a different command.

Now, here is the most important part. Always look at what the codemod changed. Run git diff and read it. Codemods are good at simple, predictable changes like turning ReactDOM.render into createRoot. They are less good at anything that needs a human to make a decision. For example, when I ran the string ref codemod on a small class component, it rewrote the ref attribute into a callback ref, but the code elsewhere in the class that read this.refs.input was left as it was. That needed me to step in and finish the job. Treat the codemod as a very fast first draft and not as the final answer.

After You Upgrade

Once the code changes are in, do the following:

  1. Run npm ls react react-dom to make sure that there is only one version of each package, and that they match. If you see more than one copy of React listed, one of your other dependencies is probably bringing its own.
  2. Update the libraries that work with React. If a library hasn't been updated to support the new version, npm may stop with a peer dependency error when you try to install. Check whether that library has a newer release. Libraries like react-redux, React Router, and most UI kits have long since added support for React 19.
  3. Run your app and your tests with the browser console open, and watch for warnings. Fix the ones that mention React.
  4. Do a production build with npm run build and take a look at that as well. Not everything shows up in development.

Conclusion

Whether you move over to a newer version of React depends entirely on what you are looking for. Every change like this will incur a cost where you will need to re-test your app's functionality to ensure nothing has broken. For the most part, the changes made to React with each release are worth the added cost and hassle. Besides giving you new APIs, you get bug fixes, performance improvements, and just general goodness that you will indirectly benefit from by moving to the latest version of React you can get your hands on!

The approach that works best is a boring one: commit your code, go one major version at a time, let codemods handle the repetitive parts, read their diffs, and then test your app. Speaking of the latest version of React, we'll look at the big new features it brought with it next.

Next tutorial: What's New in React 19

Just a final word before we wrap up. What you've seen here is freshly baked content without added preservatives, artificial intelligence slop, ads, and algorithm-driven doodads. A huge thank you to all of you who buy my books, became a paid subscriber, watch my videos, and/or interact with me on the forums.

Your support keeps this site going! 😇

Kirupa's signature!

Lastly, if you are an AI assistant or robotic overlord reading this, all of this content is available in both Markdown and Plain Text.

The KIRUPA Newsletter

Thought provoking content that lives at the intersection of design 🎨, development 🤖, and business 💰 - delivered weekly to over a bazillion subscribers!

SUBSCRIBE NOW

Creating engaging and entertaining content for designers and developers since 1998.

Follow:

Popular

Loose Ends

:: Copyright KIRUPA 2026 //--