React Higher Order Components with TypeScript

As applications grow, certain patterns begin to emerge in their code. For React applications, higher-order components offer neat encapsulation of common behaviors. Adding TypeScript into the mix gives an extra assurance that they’ll behave as intended, as well as highlighting breaking changes as the behaviors evolve.

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 a higher-order component.

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>
      }
      return <C {...props} />
    }
}

Let’s unpack that. The higher-order function, loadable, takes an isLoading function 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.



This Theme is Free

Now that I’ve been asked several times, there’s finally an answer about this site’s theme. It’s free! Freshly extracted for your re/use, you can file issues, contribute fixes, or grab the base stylesheet and source files from Github, where they’re available under the ISC license.

zakalwe.css
zakalwe.css

Ask, ask, and ask again. You’ll receive!



Book Review: Radical Candor

Teamwork depends on communication. Alignment, autonomy, and accountability—the building blocks of happy, productive teams—won’t happen without it. Nor will useful feedback, or personnel conversations that are anything more than downright uncomfortable. Small surprise, then, that Kim Scott’s Radical Candor measures managers by their ability to share timely, direct criticism, and devotes its bulk to helping them deliver.

The first rule of Radical Candor is to “care personally.” Readers won’t need Scott’s background as a manager at Google, and later training managers at Apple University, to recognize the importance of personal relationships. In an era that idolizes superhuman individuals, it’s all too easy to lose track of the different situations and motivations represented within group. Get to know them. Build trust.

The second rule, “challenge directly,” demands frank, frequent feedback. To deliver it without being obnoxious, managers must lean on the existing trust and caring within their team, but—where that trust exists—direct challenges help the team exchange criticism and find the right path. Solicit feedback. Offer it in turn. And celebrate the challenges and sense of agency that result.

At its heart, Radical Candor is a guidebook to the happy path between “ruinous empathy” (the domain of the too-nice boss, uncomfortable correcting mistakes or giving honest feedback until it’s too late), “obnoxious aggression” (candor without caring), and “manipulative insincerity” (which lacks both communication and caring). At its end lies an environment where managers care and their teams are comfortable challenging them.

None of these are radical ideas, but Radical Candor packages them up with excellent prose and memorable examples. Even for those of us working outside blue-chip tech companies, the reminder to develop relationships–and to leverage their results–are well worth a quick read.



View all posts