Migrating a codebase to React 16: some takeaways
React Hooks are the main reason why we considered upgrading our React setup across our projects, because they allow a developer to leverage the power of the React components lifecycle using only functional components. This means having components being at the same time pure functions and triggering side effects when mounted, for example. It makes sense especially if you want to extract stateful logic from a component and test it in isolation, maybe using that in multiple components too with the result of less repetition and, finally, less code (as in lines and potentially convoluted logics).
The most popular hooks are
useState, useful when it comes to stateful variables, or
useReducer, that allows a reducer to be embedded in a component in a way very similar to Redux, but without all that boilerplate. (Plus, you can still use Redux for global state management: React reducers are just another tool for a fine-grained local state management).
We began investigating how we could bring React 16 to our Hootsuite Ads codebase. It has a smaller impact surface than the global frontend codebase since it comes with its own package registry and a Storybook-based component library that we use to build our frontend applications. These applications are distributed as separated frontend apps through a CDN, or embedded as Webpack bundles inside the main Ads repository.
By the next day we had a list of things that we could do to make the process as smooth as possible. To be honest, this list was fairly short. It seemed feasible to accomplish this task, while still getting the usual day to day job done.
This was crucial, because we had a couple of release dates approaching and everything had to be in place for that time. The items in this list were split into two main themes: dependency management (like outdated and incompatible packages), and code patches removing deprecated features.
Luckily the first batch of the list was bigger than the code changes needed, thanks to some policies we adopted in advance (if you want to know more, just jump to takeaways! TL;DR — discourage new code from using legacy lifecycle methods and functions that you know will be deprecated or removed).
Cristian and Gabriele, the main developers responsible for this huge upgrade, started tackling items from time to time. It was not a lightning fast process, as we had much to accomplish this year and conducting this on top was only possible thanks to internal tools and people’s talent. We knew from the beginning that we had to be prepared for what would be a marathon, not a sprint.
We didn’t have the time to keep things bleeding edge, so when Cristian led the charge to upgrade all of our incompatible dependencies it was really tricky, mainly because at that time we still didn’t have an automated tool to keep dependencies up to date so he had to do this manually. He managed to do that in the end, and we had the first PR that laid the foundation of our React 16 roadmap.
We discovered pretty soon, in the time of a Webpack build, that this led to even further compatibility issues with our code (I’ll get to that later!). We didn’t surrender, and Cristian and Gabriele scheduled some time for the following sprint to look into this kind of trouble.
Concurrently, we had one more thing to do: while keeping packages up to date into the Hootsuite Ads repository, our component registry based on Storybook became a little outdated. During this time we first added React 16 as a peer dependency for every package of our library, then we upgraded the React-related dependencies (not only React, but also
react-dom, the test renderer and the Enzyme adapter we use for shallow rendering).
Adding React 16 as a peer dependency to our packages was a key step, it allowed us to begin upgrading React on our separate codebases (eg: the main monolith, separate frontend apps). Afterwards we removed the React 15 range, but we did it at the end of this whole process. While in the middle though, this has been a useful thing (because of its future proofness: we knew we would eventually migrate to React 16). So, we didn’t suffer any issue having some applications with React 15, and the rest of them using Fiber (the codename for React 16).
These were the two cornerstones that allowed us to go further and spread React 16 across all of our codebase.
All the preparation work, the final React upgrade and the component library upgrade brought us to the final showdown: we had to upgrade our existing code to make it work with React 16.
Unexpectedly, it was easier than expected for two reasons. The first was, the main lifecycle methods (eg.:
componentWillReceiveProps) used in our class components were still available.
Secondly, we mainly had to fix issues with PropTypes. Since Hootsuite Ads as a codebase grew so much in several years, we still used
React.PropTypes statements in some old components, and converting all these statements to raw
PropTypes ones coming from the official
prop-types package was one of the biggest pain points.
Also, we used
shallowCompare inside some components in conjunction with the
shouldComponentUpdate lifecycle method. We removed this in favour of the
React.PureComponent parent class. This was not strictly related to React 16, but we took the chance to do a bit of spring cleaning as well.
We also had to refactor all of our
connect imports, from the old one:
import connect from 'react-redux/lib/components/connect';
To the new one:
import connect from 'react-redux/lib/connect/connect';
We put a huge effort into these modifications, but the medal of honour goes to our test suites. Both our Jest test suite (which contains unit tests and so much more, like snapshot tests and functional tests on components with a lot of logic inside) and our own end-to-end test suite based on Gherkin and Behat.
Knowing we could rely on both of them allowed us to refactor code without worrying about breaking some behaviours for users. They also served to identify compatibility issues, rendering quirks, and allowed us to inspect how behaviours would be affected on a case by case.
It was like having a huge magnifying glass constantly on some point of the codebase, and we could easily classify false positives, tests that needed an update, and red flags. To be honest, it was a nice way to go through the Jest test suite too to see if something could be refactored.
After this, it was pretty much done. Using some time from our allocated slice for experiments and tech housekeeping, we only had to go for the last mile.
A trivial (not so much) takeaway
Right after the upgrade, the testing phase went pretty well and we could immediately start bringing the power of Hooks inside Hootsuite Ads. We felt like we accomplished a lot, upgrading our toolchain to a newer version of React without impacting any team or delivery date. Cristian and Gabriele were two priceless champions in this, and in the end we felt like the only takeaway we could summarize was a trivial advice, so simple to talk about but not so easy to do:
Try to keep your dependency list as slim as possible. Always fight the temptation to add a library just to use a couple of functions (or just one).
Another huge challenge is to keep dependencies up to date. Luckily, today we have so many tools for that, and once you start paying attention to automated PRs to bump package upgrades it gets easier to do this kind of major upgrade without feeling like you have to carry a giant stone over a hill, Sisyphus-style.
When you do these two things, the only thing you really need to pay attention to is the code you write. Through continuous code reviews we always tried to discourage the use of deprecated or soon-to-sunset lifecycle methods and functions. Despite being a significant overhead when developing user-facing features, this paid off so well when we faced this huge challenge, as we found our codebase really up-to-date to face the challenge.