Why and How to Upgrade React From v16.x to v18.x
At OmbuLabs and UpgradeJS we love using React to create dynamic and scalable user interfaces on the front-end of web, mobile, and desktop applications.
With the release of React v18.0 , several new features and enhancements have been added that can improve the development experience and boost the performance of React applications.
In this post, we will provide a guide to upgrading your React application from version 16.x to version 18.x, so that you can take advantage of the latest features and improvements.
It is worth noting that React version 16.x was a significant release that introduced several crucial features, including hooks , which made it easier for developers to reuse code and manage components’ behavior.
As a result, many projects still use version 16.x, making it one of the most widely used versions of React today.
- Why the upgrade matters
- Key changes and resources
- Upgrade approaches
- Test coverage
- How to upgrade
- An example of the upgrade
- Conclusion
Why the upgrade matters
There are several reasons why you should consider upgrading your React application to version 18. Let’s take a look at some of them:
Performance improvements
The upgrade to version 18.x of React can result in significant performance improvements, including enhanced memory usage , positively impacting your application’s user experience.
Bug fixes
Upgrading React can resolve bugs and issues in previous versions, enhancing your application’s stability and reliability. You can find a list of bug fixes introduced in React 18.0 in the GitHub release notes .
New features
As React introduces new versions, they may include additional features that can boost your application’s functionality. For example, React 18 introduced automatic batching , a performance enhancement that minimizes the number of renders executed.
Embrace new features and changes
Keeping your application up-to-date allows you to be prepared for upcoming features and changes. In version 18, concurrent rendering was introduced.
Although concurrency is not a standalone feature, it is a crucial mechanism that paves the way for the implementation of new features built on top of it.
Developer experience enhancement
The upgrade to the version 18.x of React can lead to enhancements in the developer experience, facilitating more efficient work with React.
For instance, the introduction of the useId hook in React 18.0 streamlines generating unique IDs.
This new feature can potentially replace the need for external libraries like uuid for this purpose.
Reduces technical debt
Upgrading to the latest version of React reduces technical debt , which refers to the accumulation of outdated or unnecessary code that may hinder development and complicate maintenance.
By keeping your codebase up-to-date, you ensure its cleanliness and efficiency, facilitating future maintenance and updates.
Key changes and resources
To learn about the changes introduced in React versions 17.x and 18.x, visit the official GitHub releases page . You’ll find detailed information on updates, improvements, bug fixes, performance enhancements, and new features for both version 17.0 and 18.0 .
Reviewing these pages will help you better understand the updates and any potential compatibility issues when upgrading. GitHub releases page is an excellent resource for developers using React and those interested in learning more about the technology.
Upgrade approaches
In this section, we will explore different approaches to upgrading your React applications and discuss their pros and cons.
"All-or-nothing"
To upgrade a React application, the most widely used strategy is the “all-or-nothing” approach, which involves upgrading the entire application at once.
However, there are a few ways to perform the upgrade. One option is to upgrade React step by step, one major version at a time. This can be helpful in identifying any issues with each upgrade and addressing them before moving on to the next version.
Another approach is to upgrade directly to the latest version. This can have some benefits, such as faster access to new features and easier maintenance.
Before deciding to upgrade in this way, it’s important to carefully consider the potential risks and drawbacks, such as compatibility issues and an increased risk of bugs.
It is worth noting that React maintainers have minimized the breaking changes in version 17.0. Therefore, you may try to perform the upgrade of your application directly to version 18.x with less concern.
Also, before using the new rendering API in version 18.x, your application will continue to work in React 17 mode.
Gradual upgrade
When upgrading your React application to version 18.x, you now have the option of using a gradual upgrade strategy, in addition to the traditional all-or-nothing approach.
With gradual upgrade, you can upgrade your application piece by piece, which can make the process smoother and less disruptive.
If your current React version is less than 17.x, you’ll need to upgrade to version 17.0 first before you can use gradual upgrade. This upgrade will involve upgrading the entire application.
However, if your current React version is greater than or equal to 17.0, you can choose between the all-or-nothing or gradual upgrade approach.
To use gradual upgrade, you’ll need to add an extra configuration to your application. React maintainers have created a demo that shows how to configure a build system to serve two different versions of React side by side in the same app.
The demo includes a helpful function called lazyLegacyRoot , which allows you to import legacy components into upgraded modern components. We recommend taking a closer look at this demo, as it includes some interesting tricks that may not be immediately obvious.
Worth noting that this approach makes sense to consider for large apps that are hard to maintain. Loading two versions of React is still not ideal (even if one of them is loaded lazily on demand). One of the big advantages is that it can reduce the risk associated with the upgrade project .
Backwards-compatible changes first
When upgrading your React application, consider taking a “backwards-compatible” approach. This involves identifying changes that can work with both your current version of React and the version you want to upgrade to.
By taking this approach, you can isolate small parts of your application and validate them using your tests. This way, you can deploy these upgraded components independently from the entire React upgrade, which can make the process smoother and less risky.
This technique can also be applied to upgrading dependencies, as long as the dependencies are compatible with both your current and desired versions of React.
An example of a “backwards-compatible” approach our case study described in the An example of the upgrade section.
Test coverage
Before upgrading a React application, one of the most critical things to consider is test coverage. It’s important to cover your application with at least end-to-end tests, focusing on the main parts and flows of your application. This will be particularly helpful during the upgrade process.
We also recommend covering your application with unit tests before the upgrade. This step can help identify bugs in your components and be beneficial during the “backwards-compatible” approach described in the Backwards-compatible changes first section.
How to upgrade
When it comes to upgrading a React application, we are fortunate to have access to amazing documentation provided by the React maintainers.
In addition to comprehensive learning materials and an excellent API reference , React also has a blog where you can find a wealth of interesting and useful information about the library.
This includes articles that can help you solve problems and highlight best practices, as well as news and announcements. If you’re looking to upgrade to React version 18.x, there’s an article that can help you do just that.
If you’re upgrading to version 17.x, there’s a release article for this version that provides possible solutions to any issues you may encounter during the upgrade from version 16.x.
There’s no need to duplicate content from the official articles, as all the pieces are essential and worth considering. However, there is an interesting case study on how the upgrade to React 18.x can look like in the An example of the upgrade section.
It’s important to note that version 17.0 includes a significant change:
the componentWillMount
, componentWillReceiveProps
, and componentWillUpdate
lifecycle methods will no longer work.
You can still use them, but you’ll need to replace them with their aliases.
The UNSAFE_*
aliases were introduced in version 16.3 to allow developers to
gradually migrate from the original methods.
You can learn more about this migration in the Update on Async Rendering article.
To address this change, you can run a
“codemod ” -
npx react-codemod rename-unsafe-lifecycles
- in the root of your React
application.
This script will rename all the required methods to their aliases with the
prefix UNSAFE_
. You can find more useful codemods in the official React
project on GitHub.
An example of the upgrade
If you started developing your application after the introduction of hooks and wrote it using only function-based components, you can expect fewer troubles when upgrading React. However, unfortunately for maintainers of legacy applications written with class-based components, the upgrade process can be more complex and complicated.
To prepare your application for the new features of React, it is necessary to eliminate side effects in the render phase lifecycle methods. This is because the new concurrent mode in React changes the way the render phase works, making it interruptible .
From this point, the only safe place to perform side effects is the commit phase.
You can find a comprehensive explanation of concurrency in React in the official article about React v18.0 , and a description of how it affects lifecycle methods in the Strict Mode official article.
Additionally, we have an article that explains how the Strict Mode can
help identify problems with your useEffect
’s .
As an example let’s consider the UNSAFE_componentWillReceiveProps
(or
componentWillReceiveProps
) lifecycle method, which runs during the render
phase. Here is a quick sample:
As you can see from the code sample, the performSideEffect
function is
executed within the UNSAFE_componentWillReceiveProps
method of the <Child />
component. This method is called when the component receives new props.
Specifically, the function is executed only if the number property in the next props is less than 4.
The outcome of this execution will be as follows:
In this case, in order to achieve the same end result using componentDidUpdate
in your React application upgrade, you will need to make the following changes:
UNSAFE_componentWillReceiveProps(nextProps) {
/* … */
// if (nextProps.number < 4) {
// this.props.performSideEffect(nextProps.number);
// }
}
componentDidUpdate(prevProps) {
/* … */
if (this.props.number < 4) {
prevProps.performSideEffect(this.props.number);
}
}
Here is the result of our changes:
In the given screenshots, it’s clear that the printed numbers and the amount of render cycles are the same for both versions. But when looking at the code and method changes, the component’s logic is now more complicated, less clear, and requires changes in not just the lifecycle method but also in other methods called from the new lifecycle method.
You’ll also see that the order in which side effects are executed has changed. While the numbers from props and render cycles are the same for this particular component, this change in side effect order might lead to unexpected bugs or issues in components that depend on the execution order of side effects and their results.
Now let’s look at another modification. To minimize unnecessary renders in React apps, developers often use the shouldComponentUpdate
method. We’ll adjust the current implementation and see how it affects the component’s flow and render cycles.
shouldComponentUpdate(nextProps) {
/* … */
// return true;
return nextProps.number < 2;
}
As a result, we will obtain the following output from the modified example that uses UNSAFE_componentWillReceiveProps
:
And the following output will be produced in an example that uses componentDidUpdate
:
The differences in these screenshots effectively demonstrate that migrating from
UNSAFE_componentWillReceiveProps
to componentDidUpdate
may not be as
straightforward as anticipated, and can even lead to unexpected outcomes in
some cases.
Conclusion
Upgrading a React application can sometimes be quite challenging. However, on the horizon, there are new React features that can significantly improve aspects like performance, stability, developer experience, and enhance your application’s functionality.
It’s definitely worth giving them a try. As soon as you begin planning your application’s upgrade, you’ll be one step closer to unlocking these great opportunities!
Need help upgrading to React 17 or 18? We can help! We provide consulting services specialized in tech debt and upgrading legacy React applications !