Step-by-step guideUpgrading to React 18

With the recent release of React 18, many existing apps will face the upgrade from an earlier React version. In this step-by-step guide I show how to upgrade a CRA-based app, which uses React Query.

The stable version of React 18 released on 29 March and was long awaited by the web development community. With its new rendering mechanism, it enables the use of concurrent features, which can improve the user experience significantly. And even without using the new features, React 18 provides out-of-the-box performance improvements.

I have a side project app which I use productively and want to keep up-to-date. I used the first chance I got to upgrade it to React 18. The app is based on Create React App (with TypeScript) and uses React Query. In this post I will go through the steps I performed for the upgrade. Note that in scope of this upgrade I only want to run the current application code on React 18. I did not yet start using any of the new, exciting features added with React 18. I will certainly look into applying these in the app in the near future.

The upgrade went more or less smooth. The biggest effort by far was adapting my tests to work with React 18: I had to change many tests to use the async queries of React Testing Library so that they would find the queried elements. I assume this to be caused by the new rendering mechanism of React 18. With the approximately 400 tests I have in this app at the moment—about half of them had to be fixed—this took me 2-3 hours.

Please also see the official upgrade guide from the React team.

Upgrade steps

In the following I describe the steps I took for the upgrade from React 17 to 18.

  1. Updating the React packages

    The first step is the actual package update to React 18.x. Since I'm using TypeScript, I needed to update the typings as well:

    $ npm install react react-dom
    $ npm install @types/react @types/react-dom
  2. Switching to the new rendering

    The library update alone does not change much. If we start the app now, it will still use the legacy rendering of React 17. This is indicated by a warning in the console:

    Warning shown by React 18 if rendered with ReactDOM.render: "ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it’s running React 17. Learn more:"

    React 18 has a new rendering mechanism which brings many improvements and enables the long-awaited concurrent features. In order to use the React 18 rendering, the new createRoot API has to be used, which replaces the former ReactDOM.render call. Therefore, the app's index.tsx has to be changed to this:

    // …
    import { createRoot } from "react-dom/client";
    // …
    const container = document.getElementById("root");
    if (!container) throw new Error("container not found!");
    const root = createRoot(container);
    root.render( <StrictMode></StrictMode> );
    // …

    As before, we retrieve the container element using getElementById.

    Then we pass it to createRoot, which has been imported from react-dom/client, to create a new root. Note that we now need to add a guard which throws an error in case the container element is not found. This is because createRoot does not allow null for the parameter.

    Lastly, we call render on the root, i.e. not on ReactDOM anymore.

  3. Updating React Query

    If just upgrading to React 18, as long as none of the new concurrent features are used, React Query can be left at v3 and only needs a minor update to v3.35. However in order to use the concurrent features, React Query would need to be updated to v4.

    At the time of writing this post, React Query v4 is still in beta, although the stable version will be released soon. If you want to update, you can execute:

    npm install react-query@4

    …and follow the migration guide for React Query 4. There are some notable API changes with React Query 4: The query keys now always need to be an array and useQueries has a different signature.

    At the point in time when I migrated my app to React 18, React Query v3 was not yet compatible with React 18, which lead to type errors when compiling the app. The support was only added a few days afterwards to v3. This is why I migrated already to v4, although it is still a beta version. Since I did not observe any issues with the React Query v4 beta, I will stay on it. With this I am ready to adopt concurrent features in my app.

    However if you want to stay on React Query v3 until v4 becomes stable, this is possible. With the React 18 support in React Query v3.35 you can run the app with the new rendering mechanism. Just be aware that you won't be able to use concurrent features with React Query v3.

  4. Updating React Testing Library to v13

    Now the app compiles again. However the tests still fail. React Testing Library added support for React 18 with v13. So let's update the library:

    npm install @testing-library/react
  5. Fixing the tests

    After this, many of my tests were still red. I noticed that I had to change many of the Testing Library queries to use the async find* API or wrap assertions in waitFor in order to make the tests pass. This was also the case for simple tests which do not use any data fetching. Therefore I assume that this is caused by how the new rendering mechanism of React 18 works (and not by changes of React Query v4).

    I was able to fix all the failed tests with this solution. This was by far the biggest effort of the upgrade to React 18.

    For working with the React Testing Library, I can generally recommend the post Common mistakes with React Testing Library by Kent C. Dodds. In this case, it was especially useful for me to read up again on his guidance on when (not) to use waitFor.

  6. Smoke test

    With all Jest tests back to green, I did a final smoke test in the app. I couldn't find any issues, so I took the upgraded app live.

    One more note: In case you use Strict Mode, as I do, you should observe well how your app behaves in development mode after the upgrade. Under these circumstances, with React 18, components are unmounted and remounted after the initial mount. In order to be future-proof for yet-to-come React features, your components should be resilient to this. With the new behavior of Strict Mode, components which have issues with this can be discovered.

Final thoughts

Upgrading an app to React 18, with its new rendering mechanism, is only the first step. The really exciting user experience improvements will come with the use of concurrent features. I hope to be able to explore these soon. I can recommend watching the React Conf 2021 talks which show what is available now and what will come soon.

How was your experience with upgrading to React 18? Let me know on Twitter!