Upgrading a large codebase to React 16

Upgrading new libraries is often a challenge. Regardless of the effort involved, there are additional unknowns of what might break as well as whether there will be actual performance benefit. In this post I’ll go through our upgrade process and share some of the tools we used.

Why upgrade?

There is an inherent benefit in staying on latest versions, especially with React and other popular open source libraries. For our SEO team, the need to upgrade was the potential for server rendering performance improvements. The feature we were most excited about was ReactDOM’s renderToNodeStream - which allows for asynchronous server rendering. We had already began to asynchronously render on the server with Formidable Labs Raspcallion, so we needed to see a significant performance improvement in order to prioritize upgrading.

Yarn resolutions

Since our codebase has gotten so large, we have ended up modularizing more of our shared code. While this has its benefits, the end result can make the process of managing dependencies more of a challenge. Thanks to a new feature in yarn called selective dependency resolutions we were able to get React 16 running locally with little effort. Yarn resolutions allows you to define specific versions across your project’s dependencies, and by adding resolutions to package.json we were able to quickly test functionality and verify performance improvements before officially proceeding with the upgrade.

"resolutions": {
    "react": "^16.0.0",
    "react-dom": "^16.0.0
}

There are definite pros and cons of using resolutions. Forcing a specific version of a dependency can be risky, but for smaller version discrepancies it can be handy in removing duplicate packages and reducing bundle size. In this case it allowed us to benchmark and identify potential issues before starting the upgrade.

Benchmarking Server Rendering Performance

Here is what we saw in early benchmarks of render time between versions of React. Clear improvement across the board, and enough of an improvement for us to justify moving forward with the upgrade.

image

JSCodeShift and react-codemod

While React 16 breaking changes are minimal, they end up being larger on older codebases. You can read through the main ones here, but for us they were mainly removing use of React.PropTypes and React.createClass. Using JSCodeShift this becomes mostly trivial. Check out this link to get started, the scripts for PropTypes and createClass would go as follows:

jscodeshift -t react-codemod/transforms/React-PropTypes-to-prop-types.js <path>

jscodeshift -t react-codemod/transforms/class.js <path>

Enzyme 3

The most difficult part of upgrading to React 16 was the required upgrade to Enzyme 3. There are still breaking changes if you don’t use Enzyme and opt for React’s test utilities, but Enzyme’s seemed to be a little trickier. Many tests required us to re-evaluate whether to use shallow or mount, and the chai-enzyme library we used required some changes to assertions along with an alpha upgrade. Check out the Enzyme 2 to 3 migration guide here.

The thrill of the upgrade

After benchmarking server rendering, making large changes with JSCodeShift and fixing a ton of tests everything was ready to merge. In order to upgrade without breaking other teams’ codebases, we needed to make sure our core frontends were ready the same day. Fortunately, our CI allows for us to easily deploy custom versions for functional testing, so there were absolutely no surprises before deployment.

Looking back, upgrading to React 16 seemed more intimidating that it actually was. The performance impact on our server rendering was exactly what we expected. With a clear approach and awesome tools for building and testing we were able to upgrade without much hassle.