React 19 Upgrade Guide

    Tuesday, April 30, 202410 min read895 views
    React 19 Upgrade Guide

    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

    Installing

    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.

    Breaking changes

    Errors in render are not re-thrown

    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 Deprecated React APIs

    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 deprecated React DOM APIs

    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.

    React Web Development made user-friendly and efficient
    Angular Minds is always providing reliable options to make your project a success. Get in touch with us and bring your project to life.

    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" />
    
    }

    New deprecations

    Deprecated: element.ref

    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.

    Deprecated: react-test-renderer

    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.

    Notable Changes

    StrictMode Changes

    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.

    UMD Builds Removed

    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>

    Libraries Depending on React Internals

    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

    TypeScript changes

    Removed deprecated TypeScript types

    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.

    ref Cleanups Required

    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.

    useRef Requires an Argument

    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);

    Changes to the ReactElement TypeScript type

    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.

    The JSX namespace in TypeScript

    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.

    Better useReducer typings

    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;

    In conclusion

    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.

    24

    Related articles

    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.