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.
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.
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:
1class Page extends React.Component {
2 state = { things: [] as string[] };
3 async componentDidMount() {
4 const things = await getThings();
5 this.setState({ things });
6 }
7 render() {
8 return (
9 <>
10 <header className="app-header">
11 ....
12 </header>
13 <div className="app-body">
14 <ul>
15 {this.state.things.map(thing => (
16 <li key={thing}>{thing}</li>
17 ))}
18 </ul>
19 </div>
20 <footer className="app-footer">
21 ...
22 </footer>
23 </>);
24 }
25}
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:
1function withHeaderAndFooter<T>(Component: React.ComponentType<T>)
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:
1function withHeaderAndFooter<T>(Component: React.ComponentType<T>) {
2 return (props: T) => (
3 <>
4 <header className="app-header">
5 ...
6 </header>
7 <div className="app-body">
8 <Component {...props} />
9 </div>
10 <footer className="app-footer">
11 ...
12 </footer>
13 </>);
14}
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 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:
1interface ThingsProps {
2 things: string[];
3}
and the function signature will then look like this:
1function withThings<T extends ThingsProps>(Component: React.ComponentType<T>)
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:
1function withThings<T extends ThingsProps>(Component: React.ComponentType<T>) {
2 return class extends React.Component<Subtract<T, ThingsProps>> {
3 state = { things: [] as string[] };
4 async componentDidMount() {
5 const things = await getThings();
6 this.setState({ things });
7 }
8 render() {
9 return <Component {...this.props as T} things={this.state.things} />;
10 }
11 };
12}
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 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:
1function helloWorld() {
2 return <div>Hello world</div>;
3}
4
5const HelloWorldPage = withHeaderAndFooter(helloWorld);
6return <HelloWorldPage />;
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:
1function helloThings(props: ThingsProps) {
2 return (
3 <ul>
4 {props.things.map(thing => (
5 <li key={thing}>{thing}</li>
6 ))}
7 </ul>);
8}
9
10const HelloThingsPage = withThings(helloThings);
11return <HelloThingsPage />;
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:
1const HelloThingsPage = withThings(helloThings);
2const FullPage = withHeaderAndFooter(HelloThingsPage);
or composed in a single line like this:
1const FullPage = withHeaderAndFooter(withThings(helloThings));
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))
.
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.