Upgrade safely with the codemods, know exactly what was removed, and adopt the new features gradually.

Bishal Biswas
Self Learner
Start on 18.3
npm install react@18.3 react-dom@18.3
React 18.3 is identical to 18.2 except that it warns about everything React 19 removes. Run your app, work through the console warnings, and the actual upgrade becomes uneventful.
Skipping this step is the main reason upgrades go badly.
Run the codemods
npx codemod@latest react/19/migration-recipe
This applies the mechanical changes automatically: ReactDOM.render, Context.Provider, forwardRef, string refs and several more.
Review the diff. Codemods are good but not infallible, and you should understand what changed.
Then upgrade
npm install react@19 react-dom@19
npm install --save-dev @types/react@19 @types/react-dom@19
What was removed
ReactDOM.render
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')).render(<App />);
Note the import path change to react-dom/client. Same for hydrate, which becomes hydrateRoot.
propTypes and defaultProps on function components
function Button({ label }) {}
Button.propTypes = { label: PropTypes.string };
Button.defaultProps = { label: 'Click' };
function Button({ label = 'Click' }) {}
Runtime prop checking is gone. Use TypeScript, or parameter defaults plus care.
Classes keep defaultProps.
String refs
<input ref="myInput" />
this.refs.myInput.focus();
const inputRef = useRef(null);
<input ref={inputRef} />
Legacy context
contextTypes and getChildContext are gone. Use createContext.
The unsafe lifecycles
componentWillMount, componentWillReceiveProps and componentWillUpdate are removed. The UNSAFE_ versions still work.
Test utilities
import { act } from 'react-dom/test-utils';
import { act } from 'react';
ReactDOM.unstable_renderSubtreeIntoContainer and react-test-renderer shallow rendering are gone too.
What changed behaviour
Errors are not re-thrown
React 18 re-threw caught errors, so they also appeared in window.onerror. React 19 does not, and gives you root options instead:
createRoot(container, {
onCaughtError(error, info) {},
onUncaughtError(error, info) {},
onRecoverableError(error, info) {}
});
If your monitoring relied on window.onerror picking up React errors, wire it up here.
useRef requires an argument
const ref = useRef();
const ref = useRef(null);
Hydration error messages
Much better in React 19, showing a diff of what differed. Nothing to change, but expect different console output.
Refs are props
const Input = forwardRef(function Input(props, ref) {
return <input ref={ref} />;
});
function Input({ ref, ...rest }) {
return <input ref={ref} {...rest} />;
}
forwardRef still works but is deprecated.
<Context> as a provider
<ThemeContext.Provider value={theme}>
<ThemeContext value={theme}>
Context.Consumer is removed entirely.
Third party libraries
The most common source of trouble, especially anything touching internals.
npm outdated
npm ls react
npm ls react shows duplicate React versions, which cause the “invalid hook call” error. Everything must resolve to one copy.
Check that your UI library, state library, router and testing tools all support React 19 before you start. A library still on forwardRef internally is fine, since it is deprecated rather than removed.
Do it in order
- Upgrade to React 18.3 and clear every warning.
- Run the codemods and review the diff.
- Check third party library compatibility.
- Upgrade React and the types.
- Run your test suite.
- Click through the app, watching the console.
- Deploy behind a flag or to staging first.
Do not adopt new features in the same change. Get the upgrade green first, then start using Actions and the rest.
Adopting the new features
Once you are on 19, take these in whatever order helps most.
Actions and useActionState replace the most form boilerplate, so start there.
ref as a prop removes forwardRef wrappers as you touch components.
Document metadata lets you drop react-helmet.
<Context> as provider tidies deeply nested providers.
The React Compiler is a separate install, and it is worth doing once the upgrade is settled.
Migrate opportunistically. There is no prize for rewriting everything in one week.
Quick recap
- Go to 18.3 first and clear every deprecation warning.
- Run the official codemod recipe and review the diff.
ReactDOM.render, string refs, legacy context andpropTypesare gone.- Errors no longer reach
window.onerror, so use the root options. useRefneeds an explicit argument.- Check
npm ls reactfor duplicate copies. - Upgrade first, adopt new features afterwards.
// STEP 1: go to 18.3 first. It is identical to 18.2 except it WARNS
// about everything React 19 removes. Clear the console, then upgrade.
// npm install react@18.3 react-dom@18.3
// STEP 2: run the codemods, then review the diff
// npx codemod@latest react/19/migration-recipe
// STEP 3: upgrade
// npm install react@19 react-dom@19
// npm install --save-dev @types/react@19 @types/react-dom@19
// ---------- REMOVED, and what replaces it ----------
// ReactDOM.render(<App />, container); note the new import path
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root'), {
// React 19 no longer re-throws caught errors, so they no longer reach
// window.onerror. Wire your monitoring up here instead.
onCaughtError(error, info) { report(error, info, { caught: true }); },
onUncaughtError(error, info) { report(error, info, { caught: false }); },
onRecoverableError(error) { console.warn('Recovered', error); }
}).render(<App />);
// propTypes and defaultProps on FUNCTION components are gone
// Button.propTypes = { label: PropTypes.string };
// Button.defaultProps = { label: 'Click' };
function Button({ label = 'Click' }) { // parameter defaults instead
return <button>{label}</button>;
}
// String refs are gone
// <input ref="myInput" /> -> const ref = useRef(null); <input ref={ref} />
// useRef now REQUIRES an argument
// const ref = useRef(); -> const ref = useRef(null);
// forwardRef is deprecated, because ref is now an ordinary prop
// const Input = forwardRef(function Input(props, ref) { ... });
function Input({ ref, ...rest }) {
return <input ref={ref} {...rest} />;
}
// Context renders as its own provider, and Consumer is removed entirely
// <ThemeContext.Provider value={theme}> -> <ThemeContext value={theme}>
// act moved out of test-utils
// import { act } from 'react-dom/test-utils'; -> import { act } from 'react';
// ---------- CHECK FOR DUPLICATE REACT COPIES ----------
// Two copies cause the "invalid hook call" error. Everything must
// resolve to one.
// npm ls react
// npm outdated
// ---------- DO THE UPGRADE FIRST, FEATURES AFTERWARDS ----------
// Then adopt, in whatever order helps most:
// Actions and useActionState removes the most form boilerplate
// ref as a prop drops forwardRef wrappers as you go
// document metadata lets you remove react-helmet
// the React Compiler a separate install, once things settle
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready