In React, higher-order components, or HOCs, are functions that take a component and output a new component after enhancing it in some manner:
1const EnhancedHOCComponent = hoc(OriginalReactComponent);
HOCs play a useful role when you have to insert functions, state and supplementary data into components or group components with styling or further JSX. The use of HOCs enables us to only extend the components that are required to be extended while keeping the codebase modular by separating the component implementation and the logic.
The thing to keep in mind here is that React components are designed to be reusable; they should be focused on a single purpose and should contain the logical implementation for that particular purpose. HOCs enable us to stick to these requirements.
The purpose of this guide is to elaborate on what HOCs are and how they are used in React. It also contains examples of their application.
HOC design pattern is applied by many modules as a way of inserting functionality from the module into the components. This is the most prevalent use case of HOCs.
Most widely used packages in React adopt HOCs design pattern, as can be seen in the following examples:
react-cookies
enable us to store and retrieve cookies from any component, the functions of which are inserted using withcookies()
HOC.react-redux
gives us connect()
HOC, which inserts state and actions as props, enabling components access the Redux store.react-router-dom
has a withRouter()
HOC that gives components router history, location and nearest matching <Route/>
.HOCs are mostly prefixed with with
or get
. HOCs prefixed using with
are mostly used to inject functionality, while HOCs prefixed using get
are mostly used to inject data in the original component.
In general, HOCs are applied to any component at export after our component has been defined.
1import { withSomeFunctions } from 'some-module';
2class OriginalReactComponent extends React.Component {
3 ...
4}
5export default withSomeFunctions(OriginalReactComponent);
It's quite easy to apply a HOC to any component; we just need to import the required HOC function and wrap our original component with the HOC. The implementation of a HOC is also the same as any function.
1export function withSomeFunctions(OriginalReactComponent) {
2 return class extends React.Component {
3 // we can make some enhancements here
4 ...
5 render() {
6 //return original react component with additional props
7 return <OriginalReactComponent {...this.props} />
8 }
9 }
10}
Thus, a new class gets created using our original component without updating the original. Actually what happens is:
render()
outputs the original component with any extra props we may want to add or any JSX we may want include.It is considered bad coding practice to update the original component or to change its prototype. Instead, best practice is to follow a composition pattern where our original component gets wrapped inside the new container class—hence the following terminology:
Note that the HOC always returns a class component. If we pass any functional component into withMyFunction()
, a new class component will be returned. The instructions on implementing functional HOCs are documented in this guide.
Since HOCs are functions, we can define them using export functions in a separate file and import it as any other function:
1// src/hocexample/index.js
2import React from 'react';
3export function withSomeFunctions(WrappedComponent) {
4 return class extends React.Component {
5 ...
6 }
7}
8// src/components/OriginalReactComponent.js
9import { withSomeFunctions } from '../hocexample';
10...
The examples above show that passing extra props into our wrapped components can be done by using spread operator in our return statement:
1render() (
2<WrappedComponent {...this.props} />
3);
This is an important first step to ensure our <WrappedComponent />
receives any new props that are passed into our HOC via JSX.
Passing props through JSX is not the only option, though. Let's say the aim of our HOC is to insert some functions for performing a specific task, e.g., to provide API calls to a web service. For this use case, we’ll import the functions and then include them as props inside our <WrappedComponent />
:
1// src/hocexample/index.js
2import React from 'react';
3import { f1, f2 } from '../someFunctions';
4export function withSomeFunctions(WrappedComponent) {
5 return class extends React.Component {
6
7 render() {
8 const updatedProps = {
9 f1: f1,
10 f2: f2
11 };
12
13 return <WrappedComponent {...this.props} {...updatedProps} />
14 }
15 }
16}
On the other hand, if we have some props that we do not want to pass through to <WrappedComponent />
, we can overcome this issue by filtering out non-required props and use spread operator again to group our remaining props:
1render() {
2 const { notNeededProp, someOtherProps, ...finalProps } = this.props;
3 return <WrappedComponent {...finalProps} />
4}
Another use of HOCs is for inserting state into components. A React class component with state has similar functionality. The following example inserts state for storing a first name. It also has a handler function to update the same.
1// src/hocexample/index.js
2import React from 'react';
3export function withSomeFunctions(WrappedComponent) {
4 return class extends React.Component {
5 constructor(props) {
6 super(props);
7 this.state = {
8 firstName: ''
9 };
10 }
11 onFirstNameUpdate = (event) => {
12 this.setState({ firstName: event.target.value });
13 }
14 render() {
15 return
16 <WrappedComponent
17 {...this.props}
18 firstName={this.state.firstName}
19 onFirstNameUpdate={this.onFirstNameUpdate}
20 />
21 }
22 }
23}
Instead of defining a newProps
object, we just pass our state related props directly to WrappedComponent
. HOCs can be used to inject values from a global store.
1constructor(props) {
2 super(props);
3 this.state = {
4 firstName: store.get('firstName') || {}
5 };
6}
7...
8render() {
9 return
10 <WrappedComponent
11 {...this.props}
12 {...this.state}
13 onFirstNameUpdate={this.onFirstNameUpdate}
14 />
15 }
16}
To avoid repetition, the official React docs have highlighted HOC abstraction of subscribing to a data source and then inserting the retrieved data into a wrapped component each time there is a change in the data, in addition to saving it in HOC’s state. The subscription to a data source itself is managed by HOC's lifecycle methods.
The HOC is called withSubscription()
, which enables to pass a DataSource
object to retrieve whatever the wrapped component concerns, e.g., a news post, a list of comments, live news feed, some logs, etc. The basis of the DataSource
is not really documented in the official React docs, and it is not very clear how it retrieves the data and listens to the changes (via polling, web sockets, etc).
But the interesting part is that a second argument is passed to our HOC, which is a function that retrieves data from an API:
1// a data source for getting list of comments
2(DS, props) => DS.getListOfComments()
3//a data source for getting news post
4(DS, props) => DS.getNewsPost(props.id)
5//a data source for getting some logs
6(DS, props) => DS.getLogs(props.id)
Thus, a function definition is inserted into our HOC to retrieve data.
The componentDidMount
and componentDidUnmount
methods have been presented to handle the initialization as well as termination of our listener, and also a handleChange
method that is invoked every time our data changes, therefore committing to the state of our component and rendering our wrapped component again.
This use case might be a bit hard to understand. These are the key points:
Component metadata is a less complex use case for HOC. It involves retrieving attributes of a wrapped component that can be inserted as props afterwards.
react-measure
or react-with-available-width
are some good examples of HOCs that measure our component width, height, bounds, etc. Both of these packages have an onResize
method, enabling us to respond to changes in any of the metrics mentioned above. These can be very useful for advanced UX customization, like reacting to window scrolling, resizing, etc.
Based on the discussion so far, we see that a HOC takes in a component and returns a new one. But this concept can be explored further. Let's say we have a function which would return another function and we can then use this to return our improved wrapped component, hereby adding another level of functionality to the HOC.
Redux’s connect()
does the same thing, as can be seen in the following syntax:
1const MyEnhancedComponent =
2 connect(mapStateToProps, matchDispatchToProps)(WrappedComponent);
connect()
returns a function and is also a function itself, and the single argument passed into that resulting function is the WrappedComponent
.
Based on the above definition, connect()
can be called a higher order function, or HOF; the output is actually an enhanced function with mapStatetoProps
and matchDispatchtoProps
, which then returns a HOC.
To elaborate this further, we are giving configuration options to higher order function to determine what is inserted into our enhanced component.
If we inspect connect()
’s method, we see 4 optional params:
1function connect(mapStateToProps, mapDispatchToProps, mergeProps, options)
The reason we have the extra functional layer is because our HOC can remain a single-argument function, making composition easier when combining HOCs together:
1const MyEnhancedComponent = withRouter(connect(mapState, mapDispatch)(WrappedComponent))
There is no restriction in adding more parameters to our HOCs for additional configuration as that is entirely valid syntax:
1function someHoc(WrappedComponent, config)
However, we are sacrificing the Component => Component
signature which is generally associated with the HOC.
The discussion so far has been around HOCs that return class components. With the introduction of functional components and React Hooks taking more priority than ever before, the question may arise whether HOCs can work with functional components too.
To implement this, we just need to return an improved function instead of returning a new class. This could be more limiting, though, than returning class components, as we’ll no longer be able to use the lifecycle methods.
However, we'll still have React Hooks. useEffect
and useState
can be used instead of using lifecycle methods and this.state
respectively.
A demonstration of improving a functional component with useState
hook is shown below. It enables the wrapped component to increase the counter provided by the HOC. The wrapped components are then inserted with counter
and setCounter
hook by the HOC itself.
1//Functional HOC using the useState hook
2import React, { useState } from 'react';
3function withCounterState(WrappedComponent) {
4 return function (...props) {
5 const [counter, setCounter] = useState(0);
6
7 props['counter'] = counter;
8 props['setCounter'] = setCounter;
9 return <WrappedComponent {...props} />;
10 }
11}
As shown above, the wrapped component will now be able to increase the counter provided by withCounterState()
. Props
will get passed as an argument here vs. using this.props
when using a class-based component.
Now the <WrappedComponent />
component has become very simple, as shown below:
1const WrappedComponent = (props) => {
2 const {counter, setCounter} = props;
3 return(
4 <div>
5 <h1>Functional Component Example for Counter</h1>
6 <p>We clicked {counter} times</p>
7 <button onClick={() => setCounter(counter + 1)}>
8 Increase counter
9 </button>
10 </div>);
11};
Finally, we apply the HOC to <WrappedComponent />
like this:
1const MyEnhancedWrappedComponent = withCounterState(Wrapped);
As functional components generally have less overhead compared to class components, they are preferred and we can switch from a class component HOC.
While most of the points we discussed here involve the benefits of HOCs, there are some caveats we should be aware of, one of them being that HOCs cannot be invoked from a component’s render method.
There are also some other edge cases mentioned in the official React docs which most developers might not need to consider, but it's still recommended to be aware of those.