Upgrading to React 19 without breaking production
The breaking changes that actually bite, the codemods that help, the TypeScript fallout, and the order we upgrade things in.
React 19 has been stable since December 5, 2024. Most of the new features — Actions, useActionState, useOptimistic, use, ref as a prop — are additive. The upgrade pain, when there is any, comes from three places: old APIs that were finally removed, TypeScript types that got stricter, and dependencies that haven’t caught up.
Here’s the order we do it in, so each step is small enough to ship on its own.
Step 0: Know what you’re running
Before anything else, list the things that can block you:
- Your framework. Next.js, Remix/React Router, Expo and Vite each have their own React 19 story. Upgrade the framework to a version that supports React 19 first, or alongside it.
- Libraries that touch React internals. UI kits, form libraries, drag-and-drop, animation, charting and rich-text editors. Check each one’s changelog for React 19 support.
- Your test setup. If you use Enzyme, plan its replacement now. It doesn’t support React 18 or 19.
npm ls react shows you which packages pin an older React as a peer dependency.
Step 1: Upgrade to React 18.3 first
React 18.3 is identical to 18.2 except that it warns about everything React 19 removes or changes. Upgrade, run the app and the tests, and read the console:
npm install --save-exact react@18.3.1 react-dom@18.3.1
Fix every warning while you’re still on 18. Each fix is safe to deploy by itself.
Step 2: Make sure you’re on the new JSX transform
React 19 requires the JSX transform introduced in 2020. Most toolchains have used it for years, but older Babel or TypeScript configs may not. If you see a warning about an “outdated JSX transform”, update your config ("jsx": "react-jsx" in tsconfig.json, or runtime: "automatic" in Babel’s React preset).
Step 3: Run the codemods
The React team publishes a migration recipe that runs the main codemods in one go:
npx codemod@latest react/19/migration-recipe
It covers:
ReactDOM.render/hydrate/unmountComponentAtNode→createRoot/hydrateRoot/root.unmount()- String refs → callback refs
actimported fromreact-dom/test-utils→actfromreactuseFormState→useActionStatepropTypes→ TypeScript types
For TypeScript projects, run the types codemod as well:
npx types-react-codemod@latest preset-19 ./src
Commit the codemod output as its own PR, separately from hand-written changes. It makes review much easier.
Step 4: Upgrade React itself
npm install --save-exact react@^19.0.0 react-dom@^19.0.0
npm install --save-exact @types/react@^19.0.0 @types/react-dom@^19.0.0
The removals that actually bite
These are the ones we see in real codebases, roughly in order of how often:
propTypes and defaultProps on function components. propTypes checks are silently removed, and defaultProps no longer applies to function components. That second one changes behavior without an error, so search for it:
// Before
function Button({ size }) { /* ... */ }
Button.defaultProps = { size: 'md' }
// After
function Button({ size = 'md' }: { size?: 'sm' | 'md' | 'lg' }) { /* ... */ }
Class components still support defaultProps.
ReactDOM.render and ReactDOM.hydrate. Gone. Use createRoot and hydrateRoot from react-dom/client. The codemod handles the simple cases; check any app that renders into several roots by hand.
ReactDOM.findDOMNode. Removed. Replace it with a ref on the element you need. This one often hides inside older third-party components — another reason to check dependencies first.
String refs and legacy context. ref="input" and contextTypes / getChildContext are removed. Use callback refs or useRef, and createContext.
react-dom/test-utils. Only act survives, and it moved to react. Everything else in test-utils now throws. If your tests use renderIntoDocument or Simulate, move them to React Testing Library.
react-test-renderer is deprecated and now logs a warning. It’s fine to keep running for a while, but plan to replace it.
The TypeScript fallout
Often the biggest diff in a React 19 upgrade comes from types, not runtime code:
-
useRefneeds an argument.useRef()is a type error; writeuseRef(null)oruseRef(undefined). All refs are now mutable, andMutableRefObjectis deprecated in favor ofRefObject. -
Ref callbacks can’t implicitly return a value, because a returned function is now treated as a cleanup:
// Type error in React 19 <div ref={(node) => (instance = node)} /> // Fine <div ref={(node) => { instance = node }} /> -
ReactElementprops default tounknowninstead ofany. Code that pokes atelement.props.whateverneeds a type argument. -
The global
JSXnamespace is gone. UseReact.JSX, and wrap custom element declarations indeclare module "react".
The preset-19 types codemod fixes most of these automatically.
Behavior changes worth knowing
- Errors are no longer re-thrown. Uncaught render errors go to
window.reportError, and errors caught by an error boundary go toconsole.error. If your error tracking relied on the old re-throw, use the newonUncaughtErrorandonCaughtErroroptions oncreateRootandhydrateRoot. - Strict Mode reuses memoized results during the development double-render and double-invokes ref callbacks on mount. Components that already follow the rules won’t notice.
element.refis deprecated becauserefis now a regular prop. Libraries that read it will log warnings until they update.
Then use the new stuff — gradually
Once you’re on 19, there’s no need to rewrite anything. We adopt new APIs where they remove real code:
refas a prop lets you dropforwardRefin new components.<Context value={...}>replaces<Context.Provider>.useActionStateand formactionfunctions replace a lot of hand-rolled loading and error state in forms.useOptimisticmakes optimistic updates a few lines instead of a small state machine.
After that, the natural next step is the React Compiler, which we cover in React Compiler in production.
Our upgrade order, as a checklist
[ ] Framework and key libraries support React 19
[ ] React 18.3 deployed, all console warnings fixed
[ ] New JSX transform confirmed
[ ] Codemods run and committed as their own PR
[ ] react, react-dom, @types/react, @types/react-dom on 19
[ ] defaultProps on function components removed
[ ] findDOMNode, string refs, legacy context removed
[ ] Tests moved off react-dom/test-utils (except act)
[ ] Error reporting checked with onUncaughtError/onCaughtError
[ ] Deployed and watched for a day before using new APIs
If you’d rather have someone do this upgrade with you, that’s what we do.
Sources
- React 19 Upgrade Guide — react.dev
- React v19 — react.dev
- types-react-codemod — GitHub