Table of contents
Open Table of contents
What is a higher-order component?
From the React docs:
A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the React API, per se. They are a pattern that emerges from React’s compositional nature.
Concretely, a higher-order component is a function that takes a component and returns a new component.
The purpose of HOCs is often to reuse common logic between components and separate logical and presentational layers. With the introduction of hooks, HOCs took a back seat for the most part. Although you see them less, HOCs are still relevant and have certain advantages over hooks, for example, better compatibility with class-based components.
const EnhancedComponent = withExtraFunctionality(WrappedComponent)
In this example, withExtraFunctionality
is the higher-order component and its goal is to add some functionality to WrappedComponent
.
Well-known examples of HOCs are the
withFormik()
function from the formik library or theconnect
function from React-Redux.
Creating a HOC
Setting up the basic functionalities
Let’s start by scaffolding our HOC first. We will create a simple HOC that adds timer functionality to our components. Our first iteration will not have strict typing, but we will tighten it up as we progress.
Here’s what the first version of our higher-order component looks like:
import { useCallback, useState } from 'react'
export function withTimer(Component: any) {
return () => {
const [count, setCount] = useState(-1)
const [timer, setTimer] = useState(-1)
const startTimer = useCallback(() => {
const timer = setInterval(() => {
setCount((previous) => {
return previous + 1
}),
1000
})
}, [])
const endTimer = useCallback(() => {
clearInterval(timer)
setCount(0)
}, [timer])
return (
<Component
startTimer={startTimer}
endTimer={endTimer}
count={count}
/>
)
}
}
At this point, our HOC is not very type-safe since we set any
for the target component’s type. There’s no type checking to make sure the component we pass accepts the props we’re trying to inject.
Now let’s start tightening the type checking.
Adding Generics
Let’s make it so that our HOC only accepts React components and that it expects the same props as the target component. We can use TypeScript generics to enforce these restraints:
import { ComponentType, useCallback, useState } from 'react'
export function withTimer<T>(Component: ComponentType<T>) {
return (hocProps: T) => {
const [count, setCount] = useState(-1)
const [timer, setTimer] = useState(-1)
const startTimer = useCallback(() => {
const timer = setInterval(() => {
setCount((previous) => {
return previous + 1
}),
1000
})
setTimer(timer)
}, [])
const endTimer = useCallback(() => {
clearInterval(timer)
setCount(0)
}, [timer])
return (
<Component
{...hocProps}
startTimer={startTimer}
endTimer={endTimer}
count={count}
/>
)
}
}
ComponentType is a special type React provides for working with components in TypeScript.
Also, notice our use of generics. We used the T
type in several places:
- We’re setting our parameter type to
ComponentType<T>
. Now, within the scope of this function,T
denotes the props type of the target component. - We’re also setting the
hocProps
type toT
to enforce that our HOC component receives the same props as the target.
Thanks to generics, TypeScript can dynamically calculate all of the props our original component accepts and enforce the same restriction for the HOC. You can learn more about generics from TypeScript docs.
Using it to enhance a component
Now let’s say we have a component like this:
import { withTimer } from './withTimer'
interface ComponentProps {
name: string;
}
function BasicComponent(props: ComponentProps) {
return (
<div>
<p>{props.name}</p>
<p>{props.count}</p>
</div>
)
}
const BasicWithTimer = withTimer(BasicComponent)
export default BasicWithTimer
Notice that we are not exporting the component, but the wrapped component returned by
withTimer
.
At this point, we have a component enhanced by the HOC, but there’s a problem.: although the HOC is adding startTimer
, endTimer
and count
, Typescript will show an error when we try to use props.count
(or any other prop injected by the HOC) because it only expects ComponentProps
.
The best way we have to solve this is to create a type for the props that withTimer
will be injecting, export that type and then extend it with ComponentProps
.
So now our HOC looks like this:
import { ComponentType, useCallback, useState } from 'react'
export interface WithTimerProps {
startTimer: VoidFunction
endTimer: VoidFunction
count: number
}
export function withTimer<T extends WithTimerProps>(Component: ComponentType<T>) {
return (hocProps: Omit<T, 'startTimer' | 'endTimer' | 'count'>) => {
const [count, setCount] = useState(-1)
const [timer, setTimer] = useState(-1)
const startTimer = useCallback(() => {
const timer = setInterval(() => {
setCount((previous) => {
return previous + 1
}),
1000
})
setTimer(timer)
}, [])
const endTimer = useCallback(() => {
clearInterval(timer)
setCount(0)
}, [timer])
return (
<Component
{...(hocProps as T)}
startTimer={startTimer}
endTimer={endTimer}
count={count}
/>
)
}
}
A couple of things are worth noticing:
- We created and exported
WithTimerProps
interface. - We changed the HOC type parameter from
T
toT extends WithTimerProps
. This change ensures that TypeScript will display an error if we try to pass a component towithTimer
whose props do not extendWithTimerProps
. - Using
Omit
utility type onhocProps
typing, we created a new type that expects all of the same props asT
except forcount
,startTimer
, andendTimer
. - Since
Omit
creates a new type, we had to use a workaround{...(hocProps as T)}
to let TypeScript know that we expect thehocProps
to be almost identical toT
except for the props we omitted.
And our component looks like this:
import { WithTimerProps, withTimer } from './withTimer'
interface ComponentProps extends WithTimerProps {
name: string;
}
function BasicComponent(props: ComponentProps) {
return (
<div>
<p>{props.name}</p>
<p>{props.count}</p>
</div>
)
}
const BasicWithTimer = withTimer(BasicComponent)
export default BasicWithTimer
A better (more complex) example
In this link there is an example project that uses a HOC, a custom hook that fetches information from an external API and has a couple of different routes.
In the README of the repository, it is explained in detail how both the higher-order component and custom hooks work. Make sure to clone the repo and make changes, as it is the best way to solidify the information from this post.