Upgrading React, Enzyme, and Typescript

Robert Mruczek
Quartet Health
Published in
4 min readFeb 21, 2019

At Quartet, we’ve been happily committed to React and Typescript for quite some time. Our team has grown a ton in the past few months, and we’re now in a much better position to take on tech debt.

In January 2019, we successfully completed our upgrade to React 16. Here are some problems that we ran into, which may save you some time if/when you find yourself doing the same.

Our versions prior to and after upgrading:

react@15.3.2 -> react@16.5.2
typescript@2.6.2 -> typescript@2.9.2
enzyme@2.8.2 -> enzyme@3.6.0

Upgrading React

The biggest, and really, only issue that we needed to solve was: how to handle the new exception handling implemented in React 16.

In React 16, if a component throws an unhandled exception, this exception will no longer simply unmount that component. Instead, the exception will continue to bubble up to parent components until a component implements the new componentDidCatch lifecycle method — what the React team is calling an “error boundary.” From the React team:

This change has an important implication. As of React 16, errors that were not caught by any error boundary will result in unmounting of the whole React component tree.

There are two ways that we handled this:

1. Implement an Error Boundary

On an unhandled exception, the following error boundary renders a component that informs the user that an error occurred, and gives information on what to do next. We decided to use a redux action for this, so that in the future, any component can simply dispatch an action to display an error if necessary.

public componentDidCatch(error: any, errorInfo: React.ErrorInfo) {
this.props.setComponentError(errorInfo); // dispatches action
}
public render() {
return (
<Wrapper>
<NavBar/>
{
this.props.hasComponentError
? <GenericErrorPage/> // error boundary
: <AuthedRoutes/> // happy path
}
</Wrapper>
)
}

Without this error boundary, the components would simply disappear on an uncaught exception in React 16, which wouldn’t be ideal.

2. We implemented a Sentry integration

Sentry is a library/service that reports browser-side exceptions wherever they occur. So far, Sentry has been extremely useful in helping us catch client-side edge cases and other errors that may have gone unreported. We don’t think we’ve seen any exceptions that are related to the upgrade yet, but better safe than sorry!

Upgrading Enzyme

There were a few breaking tests which needed to be upgraded as a result of changing APIs. Generally, this document provided by the Enzyme team will tell you what you need to know.

Here are some examples of the most prevalent cases of breaking tests in our code base, after upgrading to Enzyme 3.

  1. For mount(), updates are sometimes required when they weren’t before

This aforementioned “sometimes” case typically happens when you’re setting state on the instance of an Enzyme wrapper:

// before
wrapper.instance().setState({ displayModal: true });
expect(wrapper.find(Modal)).toHaveLength(1);
// after
wrapper.instance().setState({ displayModal: true });
wrapper.update();
expect(wrapper.find(Modal)).toHaveLength(1);

2. find() now returns DOM nodes and React nodes

Previously, find() would return only one type of these nodes depending on the circumstance. Now, it returns both host nodes and DOM nodes, so some tests that explicitly expect only one type of these nodes would fail. I suspect this was changed to eliminate ambiguity.

// before
expect(mountedWrapper.find(‘[data-test=”attribute”]’)).toHaveLength(1);
// after
expect(mountedWrapper.find(‘[data-test=”attribute”]’).hostNodes()).toHaveLength(1);

3. Private properties and methods have been removed

// before
expect(tableHeader.children().nodes[0].props.children).toEqual(‘Name’);
// after
expect(tableHeader.children().get(0).props.children).toEqual(‘Name’);

4. With shallow, getNodes() should be replaced with getElements()

// before
const propTitles = wrapper.find(ReportNote).getNodes()
.map(({ props: { title } }) => title);
expect(titles.every((title) => propTitles.includes(title))).toBe(true);
// after
const propTitles = wrapper.find(ReportNote).getElements()
.map(({ props: { title } }) => title);
expect(titles.every((title) => propTitles.includes(title))).toBe(true);

Upgrading Typescript

Upgrading Typescript was likely the most time-consuming part of this process. Here are some examples that might save you time when you’re trying to diagnose errors in React components after upgrading your Typescript version. I like to think of it this way: as Typescript becomes smarter, it’s able to figure out in more situations when types aren’t being used, or are being used incorrectly.

Examples

  1. When a destructured object is passed as an argument, you must define the type of the destructured properties.
//before
export default connect(({ patient }): OwnProps => ({
isActive: patient.isActive
}))(PatientList);
// after
export default connect(({ patient }: {patient: IReducerState): OwnProps => ({
isRecommendedPatient: patient.isRecommendedPatient
}))(PatientList);

2. You must define the type of the Generic props when a `React.SFC` is declared.

// before
const primaryPracticeFilter: React.SFC = (selectProps: SelectionFilterOptions): JSX.Element => {
// after
const primaryPracticeFilter: React.SFC<SelectionFilterOptions> = (selectProps: SelectionFilterOptions): JSX.Element => {

3. Components that use the withRouter wrapper function from react-router must extend the RouteComponentProps interface. Otherwise, the prop interfaces won’t match.


export interface OwnProps extends RouteComponentProps {
// ...
}

4. Typescript can now detect implicit anys in arrow functions on props... not that you should be doing much of that anyway.


<input type=”radio” onChange={(e: React.SyntheticEvent) => e.preventDefault()} checked={checked} />

5. This wasn’t a requirement, but Typescript now also provides a more succinct syntax for generics and their keys:

// before
export const getEnumValueByString = <T extends { [index in keyof T]: T[K] }, K extends keyof T>(enums: T, key: string): T[K] => {
const enumKey = formatCamelCaseToEnumKey(key) as K;
return enums[enumKey];
};
// after
export const getEnumValueByString = <T, K extends keyof T>(enums: T, key: string): T[K] => {
const enumKey = formatCamelCaseToEnumKey(key);
return (enums as T)[enumKey as K];
};

In Summary

We’ve been on React 16 for a few weeks now, and haven’t received any error reports or exceptions from the upgrade yet (fingers crossed). In addition, we’re now well positioned for the forthcoming 16.x minor versions with hooks! Feels good.

As usual, it’s a bit of work to get Typescript to play nicely with the React ecosystem. However, we’ve found the upsides of using Typescript to be well worth the effort so far. Hopefully this saves you some time in your next upgrade.

Interested in contributing your tech skills to help improve access to meaningful mental healthcare? Join us today! https://www.quartethealth.com/careers/

--

--

Robert Mruczek
Quartet Health

Musings about tech and politics. My views are my own.