TypeScript and React, Part 4

React Higher Order Components in TypeScript

This is part of this series of blog posts introducing TypeScript and React. Here’s Part 1.

Edit, Feb 26, 2019: Nobody on the team liked this approach as much as Render Props, so we’ve switched to that. Our experience was that HOCs can create mind-bending types.

React’s Higher Order Component pattern is a technique to help developers design better components and component interactions. Refactoring out cross-cutting concerns from a complex component with HOCs touches on at least three or four of the SOLID principles, but creating a cohesive design can be difficult without proper typing. And it’s can be especially difficult in React when using plain, old untyped JavaScript because most of the data that goes into a component gets mashed into a single untyped Props object. The more interaction there is, the harder it gets to keep track of, especially as the code changes over time. Fortunately, TypeScript provides some tools and some structure that can help keep our component design and architecture clean as our software evolves and changes.

Say, for example, that you want to create a component that displays the message “Welcome {username}” and sends the user to a “User Settings” page when that message is clicked.

The first iteration might be to write a simple component that displays the logged-in user’s information:

And here are some tests:

That’s straightforward enough. But there’s still some logic to implement:

  1. If the user is not logged in, we want to hide this component.
  2. If the user is logged in, we need to access his or her name, e.g. from Redux.
  3. We need to trigger navigation when the component is clicked, e.g. via react-router.

Where should all that functionality go? It would be easy to spew all this logic right into the Welcome component by connect-ing it to our Redux store and then also referencing the router directly. However, there are a few good reasons that this might not be a good idea:

  • We’ll likely need to personalize other components later, so this is probably a cross-cutting concern
  • The Welcome component is a dumb / presentational component, and this extra logic probably belongs in a separate smart / container component. Our simple component shouldn’t know about datastores or site navigation—it should just display data.

In terms of SOLID principles, we want to avoid adding this logic to our Welcome component because a) it would compromise its *Single Responsibility*—it’s just supposed to display a personalized link—and b) it should be *Closed to Modification but Open to Extension*—we don’t want to dig around in this component every time we need to add a new feature.

Let’s say our software already puts the user information in the Redux store under the user key. A HOC wrapper might grab that object if it exists and inject it (hey, there’s the D from SOLID too!) into the subcomponent. I would describe this functionality as “personalization”, so I’ll call this HOC withPersonalization. Here’s a first step, where the personalization is hardcoded (later we’ll introduce Redux):

There are some type declarations there that hurt the eyes (Disclaimer: if you’re a Rails dev they may land you in the hospital), so let’s remember what we’re trying to accomplish with this first iteration:

  • The goal is to provide an interface to props that a developer can see, but also have another interface to props that is only used internally (e.g. by redux). So when a developer includes our <Welcome /> component, we would like to make it clear to him that the onClick function is available (e.g via autosuggest), but we also need to ensure that the the name is hidden from him, but visible to redux’s connect.

  • When we wrap a component in withPersonalization, we want to ensure that the underlying component is aware of how personalization works. Or in other words, it needs to implement a contract that indicates it can accept personalization data—no lazy duck typing! In vanilla JS React, this contract is sort-of provided by propTypes, but in TypeScript we can accomplish a lot more.

The most important part in this code is the first type declaration. I created a type alias called HOCWrapped because I intend to reuse this, and I don’t really want to have to look a this ugly code more than once:

type HOCWrapped React.ComponentClass<PWrapped & PHoc> | React.SFC<PWrapped & PHoc>;

This type alias includes two different types, React.ComponentClass and React.SFC. Those two types are separated by a vertical bar, so taken together it is a union type meaning our withPersonalization HOC will work with either a traditional React Component Class or a Stateless Functional Component.

The powerful part of this is the ampersand: PWrapped & PHoc. PWrapped represents the type of the props that the wrapped component uses, and that we ultimately want to expose to the caller. PHoc, on the other hand, is the props type that the HOC wrapper will implement, but will hide from the caller. The two types, joined together with an ampersand, is an intersection type, meaning you pass as props an object that implements both the type PWrapped and the type PHoc.

The angle brackets around the union type indicate that we’re creating a generic type. So React.ComponentClass<PWrapped & PHoc> means “A React Component that takes props which implements PWrapped and PHoc”.

This is indeed complex and confusing, especially if you haven’t worked in typed languages like C#, Java or Scala. So let’s convert this declaration…

    type HOCWrapped<PWrapped, PHoc> = React.ComponentClass<PWrapped & PHoc> | React.SFC<PWrapped & PHoc>;

    export function withPersonalization<P, S>(
           Component: HOCWrapped<P, IWithPersonalizationProps>): React.ComponentClass<P> {
       ...
    }

… into a paragraph in English:

withPersonalization is a function that takes a component class or a higher order function as an argument, and we’ll call this argument the “wrapped component”. The wrapped component takes properties of type P-merged-together-with-IWithPersonalizationProps (i.e. a props object that’s passed in must implement both types), and the wrapped component uses a state of type S. The output of this function is another component class that accepts only objects of type P as props (and does not accept props with attributes appearing in IWithPersonalizationProps).

React wants us to stuff most everything into a component via props, so that’s why we’re going through this complex typing exercise. We want to make sure we only accept props of type P from a caller, and reject props that are of type IWithPersonalizationProps. But we also want to make sure that the wrapped component understands IWithPersonalizationProps.

This wrapper accomplishes something analogous to what destructuring assignment syntax does for variables—except that it operates on types. We’ve created a type that merges PWrapped and a PHoc into one props type, then we extracted the wrapped component’s prop type AND the HOC prop type back into their original types again. Gimme an I!

So I said I wouldn’t modify the Welcome component, but I guess that’s not the case. We won’t change functionality though—just solidify its communication with other components. We need to make sure it is expecting the personalization, so it should know about IWithPersonalizationProps. (Notice that I’m exporting the unwrapped component as well as the wrapped component as a default export, so it’s easier to test without elaborate mocking.) Here’s the second iteration of our component:

Let’s do a sanity check. When we use this thing, does it accept onClick, but not name? Here’s what it looks like in IntelliJ:

'name' is not supposed to be there: check!

'name' is not supposed to be there: check!

Fixed before we even left the editor!

Fixed before we even left the editor!

TypeScript makes propTypes obsolete.

So let’s take the next step—let’s take the user data from Redux:

In order for Redux to type the returned connected component correctly, we need to explicitly declare the ownProps, even though we don’t use it. This allows TypeScript to infer the types correctly in connect without requiring explicit definitions. (“ownProps” seems to be the standard term distinguishing the props that are defined locally by a component from props that are defined elsewhere, like redux.)

const mapStateToProps = (state: any, ownProps: P): IWithPersonalizationProps => ({
     name: state.user.name
});

Here are my tests:

All tests pass: great!

There’s still one more thing to do, which is connect to the router, so that we satisfy our last use-case. This is essentially the same strategy as our withPersonalization HOC, but we’ll also pass a second routeWhenClicked property:

Here’s the final version of the welcome control, with the cross-cutting concerns factored out into HOCs:

The last line is where all the pieces come together. You can pull in the new <Welcome /> component like this:

   import Welcome from "./welcome";

Side note: I moved the intersection type into a type alias. However, there’s one oddity—type destructuring didn’t work correctly when I stacked the HOCs together unless I included an empty “OwnProps” declaration. That’s where the interface IWelcomeOwnProps {} declaration comes from.

  • Edit: Here’s an example of replacing react-router’s missing QueryString parsing using an HOC.

Conclusion

Placing functionality into separate HOCs makes it much easier to reuse React components. Here we have two reusable HOCs and a very simple base “Welcome” component, all of which are simple to comprehend and modify. TypeScript’s compile-time (and edit-time) checks helped us separate out the concerns confidently, with the knowledge that it will be much easier to modify clean code like this in the future. And TypeScript didn’t clutter up our code, either—many of the types were inferred rather than explicitly declared.

More Reading: