Upgrading to React 16.3.X, Flow 0.53.0, and Beyond

Samantha Puth
Amplitude Engineering
7 min readSep 26, 2018

--

Upgrading React and Flow (AKA The Cost of Keeping your Engineering Team Happy)

With the speed that the Javascript community progresses, it’s hard to always be on top of the latest versions of developer tools. There are pros and cons to upgrading your dependencies and finding the right time and resources is a delicate game, if not a challenge.

At Amplitude, we value developer efficiency and developer happiness. Ultimately, the need to stay up to date, the ability to use recently developed features (e.g. Fragments!). and the possibility of using new packages (Apollo), etc. were enough to push for investing in our tech debt.

Going into this, we made sure to set expectations early on. We realistically came to terms that we may not be able to upgrade each dependency to its latest version so we came up with milestones and priorities (let’s just say we were really excited for fragments).

This aims to cover how we upgraded React, Flow, and everything in between.

But First, React

I started with upgrading React first assuming :

  1. It would be easier that Flow (hint: it was)
  2. There were potential dependency chains which would require upgrading React in order for Flow related things to work

I want to begin by saying thank you to all the other bloggers out there sharing their stories on how to upgrade React. There are a lot of great resources there already so, I’ll keep this part short and sweet.¹

I wanted to entice my teammates into reviewing my work as quickly as possible, removing as much friction as I could. In order to achieve this, I broke my work into a series of small PRs, beginning with:

  1. Upgrading deprecated libraries
  2. Removing any unnecessary dependencies (and remove any experimental dependencies
  3. Upgrading React-Router
  4. Separating each individual code mod update into their own PRs.
  5. Testing Heavily
  6. Recruiting People to test³
  7. Sending your PSA’s to your team so everyone knows how to move forward
  8. Reaping the benefits — We immediately began implementing Error Boundaries and sending any unexpected errors to Sentry. We had interesting and productive discussions on how to classify unexpected errors and when to show an error screen vs. keeping the customer in a semi-broken state. I’d share, but that’s for another time.

If there’s one thing I regret, it’s not upgrading to react-router v4. This was a difficult decision to make; our application is pretty large (~147 unique routes, 47 instances of onEnters) and I was reminded that Flow was a high priority.

Flow

I was told that this would be the bulk of my work and anticipated a few issues including:

  1. code mods weren’t fully working
  2. we use our own connect type and that was vital to resolving

My process is broken down into 4 broad steps:

  1. Running the Codemod
  2. Augmenting the code mod with search & replace for our application specific code
  3. Manually Refactor for all the broken bits
  4. Testing

I approached it with these steps:

A. Install flow via yarn global add flow-upgrade

B. Note: the version of flow-upgrade on npm does not include searching through .jsx files, to get around it:

vi ~/.config/yarn/global/node_modules/flow-upgrade/dist/findFlowFiles.js## update lines:87 — :89 here
https://github.com/facebook/flow/blob/master/packages/flow-upgrade/src/findFlowFiles.js#L92-L94

C. Run flow-upgrade and upgrade flow

flow-upgrade
yarn upgrade flow-bin@.53.0

D. Ensure that React is imported whenever a React component is rendered or executed (I used a global search and replace)

E. Update React.Children with the most correct type

Our code generally had:

const Props = {
children: React.Children,
};
  1. For most cases, React.Node was sufficient
  2. For better specificity, use React.Element
  3. For cases where the children was more than just one child, I needed to refactor the type to be React.ChildrenArray<>and pass in the correct, best specific child type
  4. Many other cases, Children was a render function that sometimes took in a parameter and rendered a Node or Element, thus being easily type-defined (ex. children: (viewportDimensions: WindowDimensions) => Node)

F. Remove all cases of DefaultProps being passed into PureComponent and Component

G. Update our Connect type

  1. We use our own specifically defined Connect type. We highly leverage this to ensure all the expected props are being passed in the right type. I had issues using the Redux Connect type because it wasn’t catching expected errors for missing props or dispatch props, thus I decided to refactor our defined type.
  2. **Note: What worked for us may not work for you
  3. First off, in our configuration, we were passing in DefaultProps into our Component. We still want DefaultProps to be validated, so the task at hand involved restructuring where it would go.
  4. The key would be resolving the resulting type to consider DefaultProps without taking it as a specific parameter
  5. Essentially, I incorporated DefaultProps into OwnProps

H. Fix all Flow Errors (via Fix known issues, investigate new issues)

  1. Spreading Props — Flow became more specific and tested for all the props as defined whereas it didn’t use to
  2. Inconsistent Multiple Type Support — Resolve cases where we were accepting/returning multiple types, especially in cases where it was string | number.⁵
  3. any types — I made it my personal goal to replace as many instances of any with a valid, more descriptive type
  4. Event Handling — I updated all event types to use React/Synthetic event types (as opposed to DOM types)⁶
  5. Refactor ref prop to be named inputRef
  6. Indescribable — We had a large handful of components where we were building an array prop value on the fly, as we rendered the component. We were using the && operator to determine whether an “action” should be added to the array. It got the job done, but, this really should be pulled as it’s own function.
bulkActions={[
hasPermission(props.orgRole, 'REMOVE_USERS') && {
callback: keys => this.setState({ pendingUsersToRemove: keys }),
labelPath: 'manageOrgUsersTable.remove',
iconName: IconNames.TRASH_18,
},
hasPermission(props.orgRole, 'CHANGE_PERMISSION_OF_USER') && {
callback: keys => this.setState({ pendingProjectAccessUsers: keys }),
labelPath: 'manageOrgUsersTable.manageProjectAccess',
iconName: IconNames.PENCIL_18,
},
].filter(Boolean)}

I. Test all difficult to resolve components

J. I kept a list of troublesome components that required more manual refactoring than average

K. Bug Safari

  1. Tell everyone that the house might be on fire
  2. Again, I reached out to my external network and recruited people to run their daily tasks on my personal staging environment. This identified the bulk of errors I introduced

There’s a light at the end of the tunnel…

What I learned from Flowing to far

As much as I would have liked to break this into smaller PRs, it became a bigger beast than powering through. If I had to redo this, I would have caught all the instances where we used the spread operator to pass in props and resolved those first, followed by updating all Children Types. Linting was my best friend. Lint errors were less intimidating and the power of linting prettified all of my code mods, and global search & replaces. This was a huge effort and my team provided moral support, solid code reviews and manual testing.

I found the lack of resources around react-redux and flow (especially regarding to Connect and type Connector) challenging. The resources I found (especially those listed here: https://flow.org/en/docs/react/redux/) gave me a great overview, but didn’t satiate me when I needed more.

Todos

This effort was fruitful, but there’s still more to go. We started at Flow v.0.49.1, upgraded to v0.53.1 and it’s only the beginning. Though I don’t regret making the decision to punt on upgrading react router, the updates make better use of React’s lifecycle and we’d like to make use of it. Since I’ve upgraded our dependencies, we’ve been playing around with Apollo and we’re hoping to use it in the near future. Ultimately, this was a huge effort and it unlocked a variety of variable tools for our developers but the most important thing it gave us was more pride in knowing that we truly own the health of our code (and tech debt).

Footnotes:

1 — Looking for more resources? Check out our Developer Center

2 — We do a lot of internal hackathons at Amplitude; some turn into viable features, others live in our experimental lab. I took the shortcut of removing the experimental work when I encountered a dependency that was incompatible with React v16

3 — To resolve my anxieties around potentially breaking things and frantically fixing them, I recruited my fellow engineers and our success team to do their daily work in my personal staging instance. This helped find bugs early on.

4 — The difference between React.Node vs React.Element is that React.Element is specifically a type of JSX Element and can take any, SpecificComponent, or * as it’s argument whereas React.Node includes React.Element amongst other things — type Node = void | null | boolean | string | number | React.Element<any>;

5 — I made the effort to resolve these instances partly because Flow threw errors in certain cases and partly for personal milestones.

6 — React uses its own event system so it is important to use the SyntheticEvent types instead of the DOM types such as Event, KeyboardEvent, and MouseEvent unless you are attaching a direct DOM Handler. In that case, you should question why you’d need it and ensure you’re removing the handler when not needed. Generally, SyntheticEvent<T> should be sufficient. If you don’t want to add the type of the element instance, you can call SyntheticEvent with no type arguments via SyntheticEvent<>. For any mouse (click, drag) events, SyntheticEvent<> will not work and you should use the correct synthetic event type

Cheers,

Sam

--

--