Command Palette

Search for a command to run...

Hectal
PHASE 23Advanced ~25 min· topic 6 of 6

Topic 23.6

Upgrading React and Keeping Current

In one line

React follows semantic versioning: patches fix bugs, minors add features, majors remove deprecated APIs after warning about them; you upgrade safely by reading the upgrade guide, moving to the last minor of your current major first, fixing every deprecation warning, running the official codemods, and keeping react, react-dom and @types/react in step.

Think of it like this

Servicing a car. A small service every few months (a patch or minor update) is quick and cheap. Skip servicing for five years and the big one becomes expensive, with parts that are no longer made. The warning lights on the dashboard (deprecation warnings) tell you what needs attention before it breaks.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Semver
Semantic versioning: MAJOR.MINOR.PATCH, where only a major may break existing code.
Caret range (^)
^19.2.8 accepts 19.2.8 and later 19.x versions, never 20.
Tilde range (~)
~19.2.8 accepts 19.2.8 and later 19.2.x patches only.
Lockfile
package-lock.json: the exact versions installed, so every install is identical.
Release channel
A stream of builds: Latest (stable), Canary (finished but unreleased features), Experimental.
Deprecation warning
A development message saying an API will be removed in a future version.
Codemod
A script that rewrites source code automatically for an API change.
peerDependencies
The versions of a package (like React) that a library says it works with.

Step by step

01See where you are

npm outdated compares installed versions with the newest allowed by your ranges ("Wanted") and the newest published ("Latest"). The table below is illustrative.

Terminal (illustrative)whole filebash
$ npm outdated
Package           Current   Wanted   Latest  Location
@types/react      18.3.12  18.3.27  19.2.18  node_modules/@types/react
react              18.2.0   18.3.1   19.2.8  node_modules/react
react-dom          18.2.0   18.3.1   19.2.8  node_modules/react-dom

$ npm ls react
shop@1.4.2
├─┬ react-router@7.9.4
│ └── react@18.2.0 deduped
└── react@18.2.0

02Step 1: the last minor of your current major

Move to the newest 18.x (18.3.1), run the app in development and the test suite, and fix every new warning. Each warning is something React 19 removes.

Terminalwhole filebash
$ npm install react@18.3.1 react-dom@18.3.1
$ npm run dev     # click through the app with the console open
$ npm test

03Step 2: the new major, with codemods

On a fresh branch, install the new versions together (and the matching types), then run the codemods and read the diff before committing.

Terminalwhole filebash
$ git switch -c upgrade-react-19
$ npm install react@^19.2.8 react-dom@^19.2.8
$ npm install -D @types/react@^19 @types/react-dom@^19
$ npx codemod@latest react/19/migration-recipe
$ npx types-react-codemod@latest preset-19 ./src
$ git diff --stat

04Step 3: verify

Type-check, lint (update eslint-plugin-react-hooks too: its newest rules follow the React Compiler, Topic 16.5), run all tests, run the app with StrictMode on, build and preview. Ship behind your normal release process and watch the error tracker for the new release (Topic 23.5).

Step 3: verifydiagram
Rendering diagram…

05Automate the small updates

A Renovate or Dependabot config opens pull requests for new versions; CI runs your checks on each. Group React packages so react, react-dom and their types always move together.

renovate.json (example)whole filejson
{
  "extends": ["config:recommended"],
  "packageRules": [
    {
      "matchPackageNames": ["react", "react-dom", "@types/react", "@types/react-dom"],
      "groupName": "react"
    }
  ]
}

Try it yourself

  1. 1

    Check more ranges

    In the first example, add "^19.2.9", "~19.2.8" and "19.2.8" (an exact version) to the list. Predict each answer before running.

  2. 2

    Fix the warnings

    In the deprecation example, rewrite OldTheme with createContext and static contextType (or a function component with use), and read element.props.ref instead of element.ref. Run it again: the console should show no warnings.

  3. 3

    Find your own app's version

    In any React app, open the browser console and look for React DevTools' Components tab, or log React.version from your code. Then run npm ls react in the project: is there exactly one copy?

Code & diagrams

Read the React version and check a semver range New tab

`version` is exported by both `react` and `react-dom`, which is handy in an error report or a debug panel. This simplified `satisfies` ignores pre-release tags (`19.3.0-canary-...`); real tools use the `semver` package. Notice `^18.3.1` doesn't allow 19: a caret never crosses a major.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

react: 19.2.8 react-dom: 19.2.8 same: true
major 19 minor 2 patch 8
^19.0.0 allows 19.2.8: true
^18.3.1 allows 19.2.8: false
~19.2.0 allows 19.2.8: true
~19.1.0 allows 19.2.8: false
^19.3.0 allows 19.2.8: false
Deprecation warnings, and a removal that stays silent React 19+ New tab

The two warnings are React 19.2's real development messages. The third problem prints nothing: `Hello, !` on screen is the only clue, which is why you read the upgrade guide and run codemods instead of relying on warnings alone. Production builds print none of these.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

props.ref works: true
Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.
element.ref still answers: true
OldTheme uses the legacy contextTypes API which was removed in React 19. Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)
screen: Theme: undefined | Hello, ! | Hello, guest!
What's in this React? Added and removed APIs New tab

Old tutorials that call `ReactDOM.render(<App />, el)` now fail with `TypeError: ... is not a function`, because the export no longer exists. Libraries sometimes use checks like these to support two React majors at once; apps should simply require one version.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

React 19.2.8
yes react: useId
yes react: useActionState
yes react: useOptimistic
yes react: use
yes react: Activity
yes react: useEffectEvent
yes react-dom: useFormStatus
no  react-dom: render
no  react-dom: hydrate
no  react-dom: findDOMNode
no  react: createFactory
What the React 19 codemod changes (excerpt)jsx
// before (React 18)
import ReactDOM from "react-dom";
import { act } from "react-dom/test-utils";
ReactDOM.render(<App />, document.getElementById("root"));

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

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Upgrade react-dom without react

Run npm install react-dom@19.2.8 but leave react on 18.3.1.

terminal
$ Browser console
── what you'll see ──
Uncaught Error: Incompatible React versions: The "react" and "react-dom" packages must have the exact same version. Instead got:
- react: 18.3.1
- react-dom: 19.2.8
Learn more: https://react.dev/warnings/version-mismatch

Break #2

Two copies of React

A local UI library in a monorepo bundles its own react, and the app uses another copy.

terminal
$ Browser console
── what you'll see ──
Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.

Myth vs fact

Myth

^19.0.0 in package.json means I might get React 20 one day.

Fact

A caret never crosses a major. You'll get 19.x updates (when the lockfile is updated), never 20.

Myth

No warnings in the console means the upgrade is complete.

Fact

Some removals are silent in the new version (React 19 ignores defaultProps on function components without warning). Read the upgrade guide and run the codemods.

Myth

Canary releases are unstable nightlies I should avoid even inside Next.js.

Fact

Canary contains features that are finished but unreleased. Frameworks pin and test a specific canary, which is supported; installing react@canary directly in your own app is what you should avoid.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    React's major upgrades are usually incremental thanks to the deprecation cycle: the warnings in 18.3 and the codemods covered most of the 18 → 19 work. Big apps upgrade a major in one branch only after shipping the last minor with zero warnings.

  • ▸

    @types/react versions track React's majors but are released separately. Some type changes (like useRef needing an argument, or JSX moving to React.JSX) break type-checking without changing runtime behaviour; types-react-codemod fixes most of them.

  • ▸

    Libraries that support several React majors use wide peer ranges ("react": "^18.0.0 || ^19.0.0") and test against each. If a library you depend on doesn't list the new major yet, check its issues before forcing it with --legacy-peer-deps or overrides.

  • ▸

    Read each release's changelog (CHANGELOG.md in the React repository and the react.dev blog), not just majors: minors can change development behaviour (for example new StrictMode checks or new ESLint rules) that your tests should cover.

Remember this

  1. 1

    Semantic versioning (semver). A version like 19.2.8 is MAJOR.MINOR.PATCH. A patch (19.2.7 → 19.2.8) only fixes bugs. A minor (19.1 → 19.2) adds features without breaking existing code: React 19.2 added <Activity>, useEffectEvent and cacheSignal. A major (18 → 19) may remove or change APIs. In package.json, "react": "^19.2.8" (a caret range) accepts any 19.x.y at or above 19.2.8 but never 20; ~19.2.8 (a tilde range) accepts only 19.2.x patches. The lockfile (package-lock.json) records the exact version installed, so every machine and CI gets the same one; commit it.

  2. 2

    React's release channels. Latest is the stable channel you install by default (npm install react), following semver. Canary (react@canary) is built from the main branch with features that are finished but not yet in a stable release; frameworks such as the Next.js App Router ship with a pinned React canary so they can use new features early. Experimental (react@experimental) contains features still being designed that may change or never ship. Applications should use Latest, or their framework's bundled React.

  3. 3

    Deprecation warnings come first. React removes an API only after warning about it in development for at least one release. React 18.3 was released specifically to add warnings for everything React 19 removed. That's why the safe path is: upgrade to the last minor of your current major, run the app and tests, fix every warning, and only then jump to the next major. Some removals don't warn at all in the new version (in React 19, defaultProps and propTypes on function components are simply ignored), so read the upgrade guide too.

  4. 4

    What React 19 removed (the big one). ReactDOM.render and hydrate (use createRoot and hydrateRoot), unmountComponentAtNode, findDOMNode, string refs, legacy context (contextTypes/childContextTypes), defaultProps for function components, propTypes checks, createFactory, react-dom/test-utils (act moved to react), and react-test-renderer/shallow. It also changed behaviour: errors in render are no longer re-thrown (they go to the root callbacks, Topic 23.5), ref became a regular prop (forwardRef is no longer needed for function components), and useRef requires an argument in the TypeScript types.

  5. 5

    Codemods do the mechanical part. A codemod is a script that rewrites your source code automatically by understanding its structure (not by search and replace). For React 19, the React team published npx codemod@latest react/19/migration-recipe, which rewrites ReactDOM.render, string refs, act imports and more, and npx types-react-codemod@latest preset-19 ./src for TypeScript type changes. Frameworks have their own (Next.js: npx @next/codemod@canary upgrade latest). Run codemods on a clean git branch, read the diff, then run your tests.

  6. 6

    Keeping current. Upgrade a little and often: patches and minors as they come (automated with Dependabot or Renovate pull requests that run CI), majors within a few months of release when your libraries support them. Before a major, check that your key libraries (router, state, data fetching, UI kit, testing) declare support in their peerDependencies. Keep react and react-dom on exactly the same version (React checks this and throws an error if they differ), keep @types/react and @types/react-dom on the same major, and make sure only one copy of React is installed (npm ls react): two copies cause the "Invalid hook call" error.

Explain it without notes

01

Explain semantic versioning and the caret and tilde ranges in package.json.

02

What are React's release channels and which should an app use?

03

Describe a safe process to upgrade a large app from React 18 to React 19.

04

Why must react and react-dom match, and what happens if two copies of React are installed?

Practice

01

Rewrite this React 18 component for React 19 without changing what it shows: function Avatar({ size }) { return <img width={size} height={size} />; } Avatar.defaultProps = { size: 48 };

02

Write a small check that throws a clear error at startup if react and react-dom versions differ.

Trade-offs

  • ↔

    Upgrading often means many small, cheap changes and access to fixes; upgrading rarely saves short-term effort but makes each upgrade large and risky.

  • ↔

    Automated update pull requests keep you current but need good tests to be trusted; without tests, they just move risk around.

Done when you can

  • I can read a semver version and explain ^ and ~ ranges.

  • I know React's Latest, Canary and Experimental channels and which to use.

  • I upgrade through the last minor and fix every deprecation warning first.

  • I can run the React 19 codemods and review their changes.

  • I keep react, react-dom and their types in step, with one copy installed.