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.8accepts 19.2.8 and later 19.x versions, never 20.- Tilde range (~)
~19.2.8accepts 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.
$ 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.002Step 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.
$ npm install react@18.3.1 react-dom@18.3.1
$ npm run dev # click through the app with the console open
$ npm test03Step 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.
$ 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 --stat04Step 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).
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.
{
"extends": ["config:recommended"],
"packageRules": [
{
"matchPackageNames": ["react", "react-dom", "@types/react", "@types/react-dom"],
"groupName": "react"
}
]
}Try it yourself
- 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
Fix the warnings
In the deprecation example, rewrite
OldThemewithcreateContextandstatic contextType(or a function component withuse), and readelement.props.refinstead ofelement.ref. Run it again: the console should show no warnings. - 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.versionfrom your code. Then runnpm ls reactin the project: is there exactly one copy?
Code & diagrams
`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
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: falseThe 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
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!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
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// 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.
Break #2
Two copies of React
A local UI library in a monorepo bundles its own react, and the app uses another copy.
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/reactversions track React's majors but are released separately. Some type changes (likeuseRefneeding an argument, orJSXmoving toReact.JSX) break type-checking without changing runtime behaviour;types-react-codemodfixes 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-depsoroverrides. - ▸
Read each release's changelog (
CHANGELOG.mdin 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
Semantic versioning (semver). A version like
19.2.8is 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>,useEffectEventandcacheSignal. A major (18 → 19) may remove or change APIs. Inpackage.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
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
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,
defaultPropsandpropTypeson function components are simply ignored), so read the upgrade guide too. - 4
What React 19 removed (the big one).
ReactDOM.renderandhydrate(usecreateRootandhydrateRoot),unmountComponentAtNode,findDOMNode, string refs, legacy context (contextTypes/childContextTypes),defaultPropsfor function components,propTypeschecks,createFactory,react-dom/test-utils(actmoved toreact), andreact-test-renderer/shallow. It also changed behaviour: errors in render are no longer re-thrown (they go to the root callbacks, Topic 23.5),refbecame a regular prop (forwardRefis no longer needed for function components), anduseRefrequires an argument in the TypeScript types. - 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 rewritesReactDOM.render, string refs,actimports and more, andnpx types-react-codemod@latest preset-19 ./srcfor 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
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. Keepreactandreact-domon exactly the same version (React checks this and throws an error if they differ), keep@types/reactand@types/react-domon 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
Explain semantic versioning and the caret and tilde ranges in package.json.
What are React's release channels and which should an app use?
Describe a safe process to upgrade a large app from React 18 to React 19.
Why must react and react-dom match, and what happens if two copies of React are installed?
Practice
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 };
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-domand their types in step, with one copy installed.