Lessons from migrating a large codebase to React 16

By Michael Greer

Facebook released a rewrite of a large portion of React last week. React 16 has been much anticipated, and the new Fiber rendering pipeline allows for a lot of performance improvements. While the React team has diligently deprecated methods and packages throughout the last version, warning us strongly in console statements to upgrade, the actual final migration is not trivial for larger codebases. We at Discord just launched our React 16 based app and wanted to share our experience and some tips we learned along the way.

The architectural direction of Fiber looks like it could eventually be a much smoother user experience for both DOM and Native renderers by chunking up the render and prioritizing quicker bits first. Right now, it isn’t clear that the advantages are significant (for one, asynchronous rendering is not enabled yet), but the changes pave the path for such gains in the future.

You are going to need to upgrade eventually. React is still in motion, and to stay behind will surely leave you fixing against bugs long abandoned by the 16+ community. Also, the new ErrorBoundary is a great addition. There are a lot of changes, and you will need to adapt.

Like all migrations, the level of pain will be directly correlated to how recent most of your codebase is. Facebook has done a good job of deprecating and warning for the last few months, but not all libraries are well-maintained and if you depend on or have forked a great many ancient ones, you will be in for a bit of a slog.

It is also up to you and your team whether this is the time to finally move to ES6 classes everywhere, or functional components, or what have you. We strongly recommend doing that after you successfully migrate, so you can understand what is breaking (and ideally covering with tests) before making it beautifully modern. Also, you will want to do this migration as quickly as possible so that you can quickly start testing and then deploying your branch. Having this sit out on a PR for a few weeks will allow master to move past you to the point that you will need to do it again.

We were starting with a good quality codebase, but one that is big enough to have not migrated off of mixins and React.createClass in the majority of components. We also pin to specific package versions, which means we are stable and confident, but our dependencies can become old and mossy.

This is where the pain came.

First and foremost, you should use the Facebook codemod to move from React.PropTypes to prop-types, and to migrate off of React.createClass. The codemod will make pretty wise choices, moving you to a class if possible (and a PureComponent class if you use the pure mixin), or shimming it with create-react-class.

Install jscodeshift, and then clone the react-codemod repo. Then run the following against your codebase:

  • Move to prop-types package:
jscodeshift -t ./react-codemod/transforms/React-PropTypes-to-prop-types.js /path/to/your/repo
  • Move to Component, PureComponent, or createReactClass:

jscodeshift -t ./react-codemod/transforms/class.js --mixin-module-name=react-addons-pure-render-mixin --flow=true --pure-component=true --remove-runtime-proptypes=false /path/to/your/repo

The latter is doing a lot of things, well described on the github page. Most importantly:

  • Moves to an ES6 class if no mixins, and PureComponent if only the pure mixin is present.
  • If a class is created, all static are migrated.
  • Creates flow annotations from propTypes.
  • Migrates method binding from constructor to arrow functions, if you like that sort of thing.
  • Skips any component using deprecated APIs like isMounted() etc.

The codemod found a number of errors in our codebase, which you should note and then go through and correct by hand.

If you do not like their codemod, or decisions, you should absolutely modify it to your taste or write your own. The codemod is an excellent tool, but not a magic wand. This alone will not migrate you!

We found that we had to search and replace a number of spots where the the codemod missed it. Spots where we had imported PropTypes from React and called it directly, perhaps. The biggest manual adjustment for us was hunting down all of the isMounted() calls and instead creating a property _isMounted which is set to true inside componentDidMount, and false inside componentWillUnmount (although maybe you shouldn’t).

Surely you don’t use private APIs from inside React? The ones from react/lib/*. If you do, the devs at Facebook have no pity for you: they have bricked you out of those.

Some of these have been moved to external libraries (the ‘react-addons-’ family of packages) but in some cases, they are just gone.

Our code has a custom TransitionGroup which depended on the flattenChildren function from inside react/lib. But this function is now gone, as are the related childMapping and mergeChildMappings functions. React devs recommend “inlining” (ie, copy-and-pasting) the required functions, which is what we eventually did. However, we inlined the newer versions of those functions from inside react-transition.

Discovering this issue, and resolving it, was the first big hurdle, but our app still did not yet run.

This one is tedious, and if you are not in a rush to get on 16 might be worth waiting until more libraries test and upgrade themselves. If you wish to help in that process, however, it will help everyone.

The console will keep warning you about packages that call React.PropTypes or use React.createClass. You will need to upgrade these if possible, replace or work around them if necessary, or fork and fix them if the package seems unmaintained.

The main issues you are looking to solve for are:

  1. Uses React.PropTypes
  2. Uses React.createClass
  3. Depends directly on pre-React 16 internals

Number 3 will lead to an error about ref ownership, and there being more than one React package. Search your yarn.lock for who is depending directly on React < 16 (they should be using a peer dependency!), and upgrade those first.

But not all package errors are so easily discovered, and herein lies the real pain.

We ran into an error which took us 2 days to track down to a library, and you may find the same. The console kept telling us that reactFiberChild could not render an element because its ref was undefined. We tore our hair out puzzling through how this could happen, and eventually dug into the React code and saw where the error was being generated. React 16 doesn’t like undefined ref attributes, but null are fine. It took us a while to discover that elements created with a core external library were being set with ref: undefined. Once we forked and fixed it, everything rendered beautifully.

Well, we also found that there were subtle errors here and there throughout the code, again mostly those which required moving through dependent libraries and upgrading or fixing them. Often upgrading a library that is fairly old will alter its rendering, blowing out your carefully crafted CSS overrides. Since most libraries have only fixed their issues once the deprecation warnings showed up in React 15.5+, only their most recent version will work. This is tedious work to discover and fix.

Moving to ES6 classes for React 16 means that you now need to be fastidious about not altering props. Where you previously got a warning about this, now it fails hard with a “Cannot assign to read only property XXX of object ‘#<XXXX>’”. If you destructure props, and try to alter those properties (or properties on those objects), this will bite you. Instead assign a new object with the updated value:

const newProp = {...prop, propertyToOverride: overrideValue}

Also, React 16 will warn about getting a boolean onClick value, which can happen if you do onClick={!disabled && this.handleOnClick}. Handle the disabled state inside the function to resolve.

We also have an iOS app which shares stores, utils, and some action creators with the web app. Migrating to React 16 was in some ways motivated by our heavy use of React Native, as newer RN releases depend on the alphas of React 16.

Unfortunately, because we need some special timers for video, we have a fork of React Native and have been back on 0.34. React 16 compatibility only arrived in RN in 0.48.0, so it is time to migrate…

We found this to be especially painful. The biggest headache was that we used React Native Webpack Server to be able to reuse code between our projects, and this project was abandoned a year ago (very hard to constantly shadow the RN codebase). So we needed to look at our build tooling, and began to migrate to Haul.

Haul keeps us in the webpack universe, but was not trivial to setup in an existing codebase. That would perhaps be the subject of another post.

Also, we have been importing iOS images using the webpack-y require(‘image!image_src’), which was no longer possible in newer versions of RN. This was grueling work, since we had to move the image assets and change how we accessed them (by importing the images directly and referencing the import). Even after writing a codemod, this was mostly manual.

The rest of the React Native migration was achieved by making a list of breaking changes from all of the intermediate RN change logs, and going through them one by one. For instance, the behavior of flex styling was (properly) changed, and as a result our usage had to be adjusted throughout the app. This was painstaking, but the most efficient path.

You will also find many classes have been deprecated. For now, we suggest you use the shim react-native-deprecated-custom-components. You may need the latest commit, since they only recently updated it for React 16.

The best bit for us was the new ErrorBoundary. Since React 16 now aborts the render tree on an uncaught error (which is a good thing), you’ll want to put something to render to the user when this happens. Additionally, the boundaries provide a great opportunity to have a central location for logging information-rich errors with both stack and component traces.

So now it renders, and your user experience is full of wonder and delight. No? We found it to be no faster than React 15, but expect this to change as we and the core devs pursue the advantages inherent in the new architecture.

The biggest advantage, like most tech debt payments, will come down the road. Our code has now been forcibly brought up to date, and this allowed us to normalize much of the code style en passant while reconsidering some of the core tooling for immediate or subsequent modernization. Performance is a bonus.

We are hiring, so come join us if this type of stuff tickles your fancy.