Author avatar

Chris Dobson

Higher Order Composition with Typescript for React

Chris Dobson

  • Apr 12, 2019
  • 9 Min read
  • 4,443 Views
  • Apr 12, 2019
  • 9 Min read
  • 4,443 Views
Web Development
React

Introduction

When building React applications, it is often the case that some component logic will need to be shared across components. There are a number of patterns that can be employed in React to achieve this and one of the most advanced, and also most popular, is the Higher-Order Component. This guide will show how to use Higher-Order Components in React using the Typescript language to ensure some type safety.

What Is a Higher-Order Component?

Higher-Order Components are similar to the Higher-Order Function pattern that is used extensively in functional programming.

Put as simply as possible, a Higher-Order Component is a function that takes a component as an argument and returns a new component. The function should be a pure function, in that it does not modify the passed component and has no other side effects, and will typically wrap the passed component in another one to add some behavior or inject some expected props, or, sometimes, both.

A more complete description of React Higher-Order Components can be found here.

Full Component

This guide will start with a React page containing a header, body, and footer that will make an API call when mounted to retrieve some data and display it in the body. This will then be used to create two Higher-Order Components, one that will show a component inside the header and footer, and one that will read the data from the API and inject the data into a component's props.

The code for the initial component is here:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Page extends React.Component {
  state = { things: [] as string[] };
  async componentDidMount() {
    const things = await getThings();
    this.setState({ things });
  }
  render() {
    return (
      <>
        <header className="app-header">
          ....
        </header>
        <div className="app-body">
          <ul>
            {this.state.things.map(thing => (
              <li key={thing}>{thing}</li>
            ))}
          </ul>
        </div>
        <footer className="app-footer">
          ...
        </footer>
      </>);
  }
}
typescript

Wrapper Higher-Order Component

The first thing to do is create a new Higher-Order Component that will take a component to be displayed in the body part of the page, put that component inside a body <div>, and also supply the header and footer elements.

The signature of this function looks like this:

1
function withHeaderAndFooter<T>(Component: React.ComponentType<T>)
typescript

The use of <T> in this signature is a Typescript generic type.In this case T denotes the type of component props passed when the Higher-Order Component is being rendered and, as no props are being injected, the component that gets returned should have props of the same type as the original. The code for the full function is here:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function withHeaderAndFooter<T>(Component: React.ComponentType<T>) {
  return (props: T) => (
    <>
      <header className="app-header">
        ...
      </header>
      <div className="app-body">
        <Component {...props} />
      </div>
      <footer className="app-footer">
        ...
      </footer>
    </>);
}
typescript

This component renders the header and footer in the same way as the full page component and renders the passed component inside the body <div>.The props passed into the Higher-Order Component are passed into this component using the object spread operator like this {...props}.

Injecting Props in a Higher-Order Component

Injecting props into a component is probably a more popular use case for a Higher-Order Component.

The original full page component calls an API to get some data and then renders it.This behavior can also be extracted into a Higher-Order Component that will make the API call when it mounts and passes the data into the supplied component via its props.

The first thing to do is define an interface describing the data as a prop:

1
2
3
interface ThingsProps {
  things: string[];
}
typescript

and the function signature will then look like this:

1
function withThings<T extends ThingsProps>(Component: React.ComponentType<T>)
typescript

In this case, the signature specifies that the props, generic type T, extends the ThingsProps interface, meaning that any component passed into this function must implement that interface in its props.

Because the things prop is to be injected by the Higher-Order Component, returning a component that accepts the same props those passed in, as in the wrapper example above, would be incorrect as any consumer of the component would need to include a things prop.One way to solve this is to use the utility-types package. This package contains a Subtract<T, T1> operator that takes a type T and removes any properties from it that exist in type T1.This Higher-Order Component could then return a component with props of type Subtract<T, ThingsProps>, meaning that consumers would not need to supply a things prop just to satisfy the Typescript compiler.

The code for the function is here:

1
2
3
4
5
6
7
8
9
10
11
12
function withThings<T extends ThingsProps>(Component: React.ComponentType<T>) {
  return class extends React.Component<Subtract<T, ThingsProps>> {
    state = { things: [] as string[] };
    async componentDidMount() {
      const things = await getThings();
      this.setState({ things });
    }
    render() {
      return <Component {...this.props as T} things={this.state.things} />;
    }
  };
}
typescript

Where the header and footer function returned a functional component, this component needs state and so returns a class component. The state definition and componentDidMount method are identical to those in the original full page component. When rendering this component, as well as passing all of the props passed into the Higher-Order Component using {...this.props}, the things prop is also set to the current value of this.state.things, so when consuming this component the things prop will be populated with the data from the API call. In this case, this.props must be cast to type T as otherwise, the Typescript compiler will throw an error.

Consuming a Higher-Order Component

Consuming these new Higher-Order Components is, as would be expected, a case of calling the function with an existing component with the correct props and rendering the result of the function. So the withHeaderAndFooter Higher-Order Component can be used like this:

1
2
3
4
5
6
function helloWorld() {
  return <div>Hello world</div>;
}

const HelloWorldPage = withHeaderAndFooter(helloWorld);
return <HelloWorldPage />;
typescript

The HelloWorldPage component is now made up of a header, body, and footer with the text 'Hello world' displayed in the body.

Passing the helloWorld component to the withThings Higher-Order Component will result in an error from the Typescript compiler because withThings requires a component with a prop of things.This Higher-Order Component can be used like this:

1
2
3
4
5
6
7
8
9
10
11
function helloThings(props: ThingsProps) {
  return (
    <ul>
      {props.things.map(thing => (
        <li key={thing}>{thing}</li>
      ))}
    </ul>);
}

const HelloThingsPage = withThings(helloThings);
return <HelloThingsPage />;
typescript

As discussed when creating the withThings component, there is no need to pass the things prop when rendering as this is taken care of by the Higher-Order Component.

To create a component composed of both the withHeaderAndFooter and withThings Higher-Order Components is simply a question of passing the result from one to the other.So, creating a component that wraps the helloThings component inside a page with header, footer, and body that also injects the things, can be done like this:

1
2
const HelloThingsPage = withThings(helloThings);
const FullPage = withHeaderAndFooter(HelloThingsPage);
typescript

or composed in a single line like this:

1
const FullPage = withHeaderAndFooter(withThings(helloThings));
typescript

and rendered <FullPage />. The resulting FullPage component is now the same as the original component.

The order in which the Higher-Order Components are called makes no difference, the same result can be achieved by withThings(withHeaderAndFooter(helloThings)).

Conclusion

The reuse of component behavior in React can be achieved by using the Higher-Order Component pattern to compose new components. This guide has given some examples of how to implement this pattern using Typescript and an example application using the Higher-Order Components in this guide can be found here.

18