The latest version, React 19 Beta was unveiled last Thursday. It is available on the npm and in our previous blog, we talked about the new features and how developers can adopt them. In the React 19 beta upgrade guide, we will discuss the improvements added to React 19. Though the latest version requires some breaking changes but React has done its best to make the process smooth and effective.
The following steps will help you upgrade your libraries to React 19 beta:
Installing
Breaking changes
New deprecations
Notable changes
TypeScript changes
If developers want to install the latest version of React and React DOM:
npm install react@beta react-dom@beta
If you make use of typescript, it will also need an upgrade. You can get the types as usual from @types/react and @types/react-dom, once React 19 gets stable. During the beta period, the types are available in different packages that will require enforcement in your package.json:
{
"dependencies": {
"@types/react": "npm:types-react@beta",
"@types/react-dom": "npm:types-react-dom@beta"
},
"overrides": {
"@types/react": "npm:types-react@beta",
"@types/react-dom": "npm:types-react-dom@beta"
}
}
They also included a codemod for the most common replacements.
In the previous versions of React, errors that were thrown during rendering were caught and rethrown. The error handling is improvised to reduce duplication through no re-throwing:
Uncaught Errors are Errors that are not caught by an Error Boundary and are reported to window.reportError.
Caught Errors are the errors that are caught by an Error Boundary and are reported to console.error.
This change won't have a huge impact on most apps, but if your production error reporting depends on errors being re-thrown, you may need to alter your error handling. To support this, a new method is added to createRoot and hydrateRoot for custom error handling:
const root = createRoot(container, {
onUncaughtError: (error, errorInfo) => {
// ... log error report
},
onCaughtError: (error, errorInfo) => {
// ... log error report
}
});
Removed: propTypes and defaultProps for functions
The propType checks from the React package are removed, and using them will make no difference. If you like to use propTypes, it is recommended to TypeScript or another type-checking solution.
defaultProps is also deleted from function components instead of ES6 default parameters. Class components will continue to support defaultProps as there is no ES6 substitute.
// Before
import PropTypes from 'prop-types';
function Heading({text}) {
return <h1>{text}</h1>;
}
Heading.propTypes = {
text: PropTypes.string,
};
Heading.defaultProps = {
text: 'Hello, world!',
};
// After
interface Props {
text?: string;
}
function Heading({text = 'Hello, world!'}: Props) {
return <h1>{text}</h1>;
}
Removed: Legacy Context using contextTypes and getChildContext
In React 19, Legacy Context is removed to make the React app minutely faster.
// Before
import PropTypes from 'prop-types';
class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string.isRequired,
};
getChildContext() {
return { foo: 'bar' };
}
render() {
return <Child />;
}
}
class Child extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
};
render() {
return <div>{this.context.foo}</div>;
}
}
// After
const FooContext = React.createContext();
class Parent extends React.Component {
render() {
return (
<FooContext value='bar'>
<Child />
</FooContext>
);
}
}
class Child extends React.Component {
static contextType = FooContext;
render() {
return <div>{this.context}</div>;
}
}
Removed: string refs
String refs are removed in this version to make React simple to use and easy to understand. If developers are still using string refs in class components, they will need to migrate to ref callbacks:
// Before
class MyComponent extends React.Component {
componentDidMount() {
this.refs.input.focus();
}
render() {
return <input ref='input' />;
}
}
// After
class MyComponent extends React.Component {
componentDidMount() {
this.input.focus();
}
render() {
return <input ref={input => this.input = input} />;
}
}
Removed: Module pattern factories
The support for module pattern factories is removed in the latest version and developers will need to migrate to regular functions:
// Before
function FactoryComponent() {
return { render() { return <div />; } }
}
// After
function FactoryComponent() {
return <div />;
}
Removed: React.createFactory
It was a common practice to use createFactory before broad support from JSX. However, it is rarely used these days and can be easily replaced by JSX. So in React 19, createFactory is removed and you will require to migrate to JSX:
// Before
import { createFactory } from 'react';
const button = createFactory('button');
// After
const button = <button />;
Removed: react-test-renderer/shallow
In React 19, an upgrade is made from react-test-renderer/shallow to re-export react-shallow-renderer. In React 19, react-test-render/shallow is omitted and to prefer installing the package directly:
npm install react-shallow-renderer --save-dev
- import ShallowRenderer from 'react-test-renderer/shallow';
+ import ShallowRenderer from 'react-shallow-renderer';
Removed: react-dom/test-utils
act is moved from react-dom/test-utils to the react package. The rest of the test-utils functions have also been removed. These utilities were rare and made it effective to depend on low-level implementation details of your components and React.
Removed: ReactDOM.render
In this version, ReactDOM.render is removed and developers will need to migrate to using ReactDOM.createRoot:
// Before
import {render} from 'react-dom';
render(<App />, document.getElementById('root'));
// After
import {createRoot} from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
Removed: ReactDOM.hydrate
Since the ReactDOM.hydrate is removed, you’ll need to migrate to using ReactDOM.hydrateRoot:
// Before
import {hydrate} from 'react-dom';
hydrate(<App />, document.getElementById('root'));
// After
import {hydrateRoot} from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
Removed: unmountComponentAtNode
Developers will need to shift to using root.unmount().
// Before
unmountComponentAtNode(document.getElementById('root'));
// After
root.unmount();
Removed: unmountComponentAtNode
In this version, you’ll need to migrate to using root.unmount() as ReactDOM.unmountComponentAtNode was deprecated.
// Before
unmountComponentAtNode(document.getElementById('root'));
// After
root.unmount();
Removed: ReactDOM.findDOMNode
The findDOMNode is deprecated due to a legacy escape hatch that was not fast enough to execute, weak to refactoring, only returned the first child, and broke abstraction levels. You can replace ReactDOM.findDOMNode with DOM refs:
// Before
import {findDOMNode} from 'react-dom';
function AutoselectingInput() {
useEffect(() => {
const input = findDOMNode(this);
input.select()
}, []);
return <input defaultValue="Hello" />;
}
// After
function AutoselectingInput() {
const ref = useRef(null);
useEffect(() => {
ref.current.select();
}, []);
return <input ref={ref} defaultValue="Hello" />
}
React 19 supports ref as a prop, so the element.ref in place of element.props.ref. is deprecated. Accessing element.ref will warn:
Accessing element.ref is no longer supported. ref is now a regular prop. It will be removed from the JSX Element type in a future release.
react-test-renderer is deprecated as it implements its own render environment which is not the same environment users utilize, promotes testing implementation details, and depends on introspection of React’s internals.
In React 19, the react-test-renderer notifies a deprecation warning and has shifted to concurrent rendering. It is suggested to shift your tests to @testing-library/react or @testing-library/react-native for a modern and well-supported testing experience.
There are a couple of improvements in the Strict Mode. When developers double render in strict mode in development, the useMemo and useCallback will re-utilize the memoized results from the first render in the second render process. Components that are already compatible with strict mode shouldn't find a major difference in their behavior.
This feature is designed to actively surface bugs in your React app components during the development, so they get fixed before they are pushed for production.
There are modern substitutes for loading modules as scripts in HTML documents. Initiating with React 19, React will not create UMD builds to diminish the challenge of its testing and release process.
It is advised to employ an ESM-based CDN such as esm.sh to load React 19 with a script tag.
<script type="module">
import React from "https://esm.sh/react@19/?dev"
import ReactDOMClient from "https://esm.sh/react-dom@19/client?dev"
...
</script>
React 19 includes changes in React Internals that may impact libraries that ignore the request of not using internals like SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED. These changes are crucial to bring the needed improvements in React 19.
Based on the Versioning Policy, React didn't include these components in the breaking changes, and there are no documents including insights on how to upgrade them. The suggestion and aim here is to remove any code that depends on internals.
To create an impact of using internals, they renamed the SECRET_INTERNALS suffix to:
DONOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
Based on the removed APIs in the React 19, the typescript types are also removed. A few of them are shifted to more relevant packages while others are discarded.
This component is included in the React 19 codemod preset as no-implicit-ref-callback-return.
Due to the introduction of ref cleanup functions, sending anything else from a ref callback will not be taken by TypeScript. The fix is usually to stop using implicit returns:
- <div ref={current => (instance = current)} />
+ <div ref={current => {instance = current}} />
The real or original code incorporates the instance of the HTMLDivElement and TypeScript wouldn’t know if this was to be a cleanup function or not.
The above change is in React 19 codemod preset as refobject-defaults. . This significantly makes easier its type signature. It’ll now behave more like createContext.
// @ts-expect-error: Expected 1 argument but saw none
useRef();
// Passes
useRef(undefined);
// @ts-expect-error: Expected 1 argument but saw none
createContext();
// Passes
createContext(undefined);
The props of React elements automatically default to unknown instead of any of the known elements being typed as ReactElement. This won't be a problem for you if you pass a type argument to ReactElement:
type Example2 = ReactElement<{ id: string }>["props"];
// ^? { id: string }
But if you depended on the default, you now have to handle unknown:
type Example = ReactElement["props"];
// ^? Before, was 'any', now 'unknown'
Developers will only require it if they have added a lot of legacy code depending on unsound access to element props. Element introspection occurs as an escape hatch, and developers have to make it exclusive that your props access is unsound via an explicit any.
This change is added in the react-19 codemod preset as scoped-jsx. This will assist us in reducing pollution of global types that refrain conflicts between different UI libraries that leverage JSX.
You’ll now need to wrap module augmentation of the JSX namespace in `declare module ”…”:
// global.d.ts
+ declare module "react" {
namespace JSX {
interface IntrinsicElements {
"my-element": {
myElementProps: string;
};
}
}
+ }
The exact module specifier relies on the JSX runtime you specified in the compilerOptions of your tsconfig.json:
For "jsx": "react-jsx" it would be react/jsx-runtime.
For "jsx": "react-jsxdev" it would be react/jsx-dev-runtime.
For "jsx": "react" and "jsx": "preserve" it would be react.
useReducer now has improved type inference because of @mfp22.
However, a breaking change is required in places that have useReducer and don’t accept the full reducer type as a type parameter but instead either need none or need both the state and action type.
The new best practice is not to pass type arguments to useReducer.
- useReducer<React.Reducer<State, Action>>(reducer)
+ useReducer(reducer)
This will definitely not work in edge cases where you can directly type the state and action, by passing in the Action in a tuple:
- useReducer<React.Reducer<State, Action>>(reducer)
+ useReducer<State, [Action]>(reducer)
If you define the reducer inline, you are motivated to annotate the function parameters instead:
- useReducer<React.Reducer<State, Action>>((state, action) => state)
+ useReducer((state: State, action: Action) => state)
This is also what you will need to incorporate if you move the reducer outside of the useReducer call:
const reducer = (state: State, action: Action) => state;
The upgrade guide aims to optimize the performance of React app with concurrent rendering. You will also benefit from it as their primary motive is to enhance developer experience with the help of react compiler and Refs for Functional Components. With improved Code Splitting and integration with Web Components, the reusability, and maintainability will also stay apt.
This website uses cookies to analyze website traffic and optimize your website experience. By continuing, you agree to our use of cookies as described in our Privacy Policy.