React Higher Order Components with TypeScript

It’s hard to imagine React Higher-Order Components (HOCs) before static types came along. For React applications, higher-order components offer neat encapsulation of common behaviors. But TypeScript is the secret sauce: with static guarantees in the mix, we can know for certain that an HOC is behaving as intended.

A first pass

Take data loading. As polite developers, we’ll want to show a loading state while we await the result of user-initiated API requests.

import * as React from 'react'

const Loader: React.SFC<{}> = () =>
  <div>Just a moment, please...</div>

type Props = {
  isLoading: boolean,
  count: number,
}

const Counter: React.SFC<Props> = ({ isLoading, count }) => {
  if (isLoading) {
    return <Loader />
  }

  return <div>
    <h1>Count is {count}</h1>
  </div>
}

Assuming for simplicity’s sake that isLoading is being appropriately managed by a state container elsewhere in the app, our stateless functional component will either show a loading indicator or a newly-updated count.

Refactor

Notice the two separate concerns? While the view logic is specific to the counter component, loading states are a somewhat more general problem. Once multiple components across the application need to wait on API requests, we can save some conditional boilerplate and bespoke implementations by generalizing their collective behavior into an HOC.

For a crude first pass, let’s extract the conditional logic verbatim into a new component.

interface LoadableProps {
  isLoading: boolean,
}

function loadable<P extends LoadableProps>
  (C: React.ComponentClass<P>|React.SFC<P>): React.SFC<P> {
    return (props) => {
      if (props.isLoading) {
        return <Loader />
      }

      return <C {...props} />
    }
  }

Now that the details of loading are encapsulated inside the loadable component, the Counter can focus entirely on presenting data. Composing them yields a LoadableCounter with the same behavior as our original Counter.

type Props = LoadableProps & {
  count: number,
}

const Counter: React.SFC<Props> = props => (
  <div>
    <h1>Count is {props.count}</h1>
  </div>
)

const LoadableCounter = loadable(Counter)

But this isn’t a very satisfying implementation. The clue is in the constraint that the generic loadable imposes on “child” components.

interface LoadableProps {
  isLoading: boolean,
}

Any child implementing the loadable behavior will need LoadableProps in addition to its own.

type Props = LoadableProps & {
  count: number,
}

This may not be a problem if we use isLoading everywhere, but it’s at odds with our effort to cleanly separate loading behavior from rendering.

Generalize

With a few small adjustments, we can break our dependency on isLoading and rewrite loadable in complete ignorance of the “child” component’s props.

function loadable<P>(isLoading: (p: P) => boolean) {
  return (C: React.ComponentClass<P> | React.SFC<P>): React.SFC<P> =>
    (props) => {
      if (isLoading(props)) {
        return <div>Just a moment, please...</div>
      }

      // Set pretty `displayName` for debugging and dev tooling
      C.displayName = `Loadable(${C.name})`
      return <C {...props} />
    }
}

Let’s unpack that. The higher-order function, loadable, accepts a predicate (isLoading) that maps component props to a boolean. The request is either in progress, or it isn’t.

We can now isolate loading state from the details of individual props:

type Props = {
  count?: number,
}

const isCounterLoading = (p: Props) =>
  typeof count === 'number';

Beneath the mapping function, loadable functions just as before. Pass in a component and get a Loadable version back.

const Counter: React.SFC<Props> = props => (
  <div>
    <h1>Count is {props.count}</h1>
  </div>
)

const LoadableCounter = loadable(isCounterLoading)(Counter)

Conclusion

While the final loadable implementation (github example) is marginally more complex than the naive first pass, adding a mapping function means more flexibility at the component interface. To reimplement the original loadable component, for instance, we need only call loadable with an appropriate mapping.

const originalIsLoading = (p: LoadableProps) => p.isLoading
const originalLoadable = loadable(originalIsLoading)

It’s the best of both worlds: a clean break between view and behavior, and a family of higher-order components that are easily reused where needed.

Featured