Chris M.
7 Feb 2022
•
7 min read
A HOC can be imagined as a component which is passed to some function. That function would basically return an enhanced version of that base component.
There are various use cases where we can use an HOC. Some of them include;
Infinite scrolling in different views, each having it’s own data set
Components that use data from any kind of third party library
Components which need to display data of logged in user
Showing lists e.g. Users list, List of locations, etc and with a search feature
Enhancing the card view in any app with same sort of border or shadow effects
For all of the above use cases, React has a way to share common logic across multiple different components without the need to rewrite via the use of Higher-Order Components or what are commonly known as HOCs
React’s Higher-Order Components (HOC) pattern is primarily from React’s feature that prefers composition over inheritance. We can look at the following examples;
// Example 1
const twice = (f, v) => f(f(v))
const adder2 = x => x + 2
twice(adder2, 6) // 10
// Example 2
const filter = (predicate, xs) => xs.filter(predicate)
const is = type => y => Object(y) instanceof type
filter(is(Number), [1, '2', 3, null]) // [1, 3]
// Example 3
const withMyCounter = fn => {
console.log(`This is a example to demonstrate the usage of withMyCounter`)
let counter = 0
return (...args) => {
console.log(`Final Counter value is ${++counter}`)
return fn(...args)
}
}
const add = (a1, a2) => a1 + a2
const counterSum = withMyCounter(add)
console.log(counterSum(20, 30))
console.log(counterSum(22, 11))
console.log(counterSum(220, 110))
// Output -
// This is a example to demonstrate the usage of withMyCounter
// Final Counter value is 1
// 50
// Final Counter value is 2
// 33
// Final Counter value is 3
// 330
Below is an example of a basic HOC.
const withSomeParam = (BaseComponent) => {
class HOC extends React.Component {
render() {
// Here we set the value for some––Param to 99 and the same would be available in other components
return (
<BaseComponent
{...this.props}
someParam={99}
/>
);
}
}
return HOC;
};
export default withSomeParam;
To use the above HOC, we can define any component as shown below;
import withSomeParam from 'components/withSomeParam';
const DisplayTheParam = props => (
<div>
The value of param is {props.someParam}.
// Here we get the value of someParam as 99 via the HOC we had defined above
</div>
);
const WrappedComponent = withSomeParam(DisplayTheParam);
export default WrappedComponent;
Here, we first create a new WrappedComponent which gets the prop someParam. DisplayTheParam itself has no someParam prop. As a best-practise, we should try and use variables that have already been defined above. So in this case, DisplayTheParam should be defined above it's actual usage. However, we can use it whichever way we want since variables as well as functions get automatically hoisted to the top of our scope.
We are able to get this to work, but it might give an error at runtime in case we decide to use DisplayTheParam component without first wrapping it in a HOC.
Also, our WrappedComponent consists of DisplayTheParam as well as HOC composition.
There are different ways of using HOC. One is via using an HOF (higher-order function as can be seen below)
So we have a withApi() HOF in the example below;
const MyEnhancedComponent = withApi()(MyBaseComponent)
Now, in this case, it seems like extra work to have this usage as withApi()(MyBaseComponent) instead of just withApi(MyBaseComponent)
And that seems right in this case. Let’s refactor the same as shown below.
Using Class Component as wrapper
export function withApiClass(BaseComponent) {
return class MyApiUrls extends React.Component {
constructor(props) {
super(props)
this.myapi = 'http://www.mywebsite.com'
}
render() {
return <BaseComponent api={this. myapi} {...this.props} />
}
}
}
To use the above HOC, all we can do is
const Component1 = withApiClass(BaseCmp)
Other option is using function component as wrapper
Option with function component
export function withApiFunction(BaseComponent) {
return function ApiUrls(props) {
const api = useRef('http://www.test.com')
return <BaseComponent api={api.current} {...props} />
}
}
To use the above HOC, we can write
const Component1 = withApiFunction(BaseCmp)
If it is possible to have a simple usage as shown above for the HOCs, then why do we need the HOF that we had seen in the first example?
Well, we may or may not need the HOF depending on our specific requirement. One of the use-case is to pass in some config object to the HOF.
Below is an example;
export function withApi(config) {
const { isSiteSecure } = config
return function (BaseComponent) {
class MyApiUrls extends React.Component {
constructor(props) {
super(props)
this.myapi = `http${ isSiteSecure ? 's' : ''}://www.mywebsite.com`
}
render() {
return <BaseComponent api={this. myapi } {...this.props} />
}
}
return MyApiUrls
}
}
To use the above HOF, we can use the following syntax
const Component1 = withApi({ isSecure: false })(BaseCmp)
If we notice, the above syntax is also used in Redux where we have the connect HOF that also takes in a few arguments and is similar to the HOF we have used above. So basically, the idea of writing HOF’s is that we separate config param from our component param and that allows anyone to easily use the compose function whenever we have multiple HOC’s to consume. Thus, a HOF is actually a higher-order function and that literally means that it returns a function which when invoked returns to us a component.
We have also seen that when we invoke the returned function, it can either return a class component or a function component. We have seen both possibilities in the examples above. The above way of using HOC allows us to compose. We can use any sort of compose utility like Redux, Lodash,, Ramdajs, etc to be able to create a new composed HOC whenever we use multiple HOC’s or HOF’s. Thus, we are able to maximize composability using the above way of defining our HOC. Now, if you do not like the HOF pattern, but still if you want composition and configuration, can we achieve that. The answer is yes. We can take a look at an example of the same below;
export function withMyApi(BaseComponent, config) {
const { isSecure } = config
return function ApiUrls(props) {
const api = useRef(`http${isSecure ? 's' : ''}://www.test.com`)
return <BaseComponent api={api.current} {...props} />
}
}
Now, how can we consume the above. For that, let’s have a look at the following code;
const Component1 = withApi(BaseCmp, { isSecure: true })
/* OR if we want to consume more HOC's.
Nested components/ may be dirty? */
const Component1 = withRouter(withApi(BaseCmp, { isSecure: true }))
-----------------------------------------------------------
// But we cannot compose as below:
const myComposedHOC = compose(
withRouter, // a function which is fine
withApi(BaseCmp, { isSecure: false }) // NOT fine as it would be a React Component now.
)
/* It would give an error when we call myComposedHOC(...). That is because, in React, we do not call components using () ,
but instead it is invoked via the angle brackets i.e. < /> */
/* After composing, we would not be able to run the below line as that would pass “BaseCmp” twice to “withApi” . This can get quite confusing !
Also we would see errors due to calling a React Component using parenthesis () as mentioned above */
const Component1 = myComposedHOC(BaseCmp)
-----------------------------------------------------------
// But we can compose as shown below:
const myComposedHOC = compose(
withRouter,
withApi
)
const Component1 = myComposedHOC(BaseCmp, { isSecure: false })
/* This would work, as it would pass 2 params - "BaseCmp" & "{ isSecure: false }" to LAST HOC i.e. withApi.
Also the result of that would get passed to FIRST HOC (withRouter) */
-----------------------------------------------------------
// But, surprisingly, there would be an error if we update the order as below:
const myComposedHOC = compose(
withApi, // FIRST
withRouter // LAST
)
const Component1 = myComposedHOC(BaseCmp, { isSecure: false })
/* Because the above would pass the 2 parameters to LAST HOC (i.e. withRouter),
and the result of that to the FIRST HOC (i.e. withApi). So, withApi would
receive only enhanced "BaseCmp" and not the "{ isSecure: false }". This is the reason why
using "compose" is not reliable for HOC's that accpet config params */
/* Just to add, this is how a compose function works :
"It basically composes single-argument functions from right to left. The function on the right can take in multiple args, as it gives the signature to the final composed function"
i.e. compose(x, y, z) is the same as (...args) => x(y(z(...args)))
Thus, it passes the args to z and the result to y and then result to x. */
In case, you like the HOF way, then the same can be defined as below;
export function withMyApi(config) {
const { secure } = config
return function (BaseCmp) {
return function ApiUrls(props) {
const api = useRef(`http${secure ? 's' : ''}://www.test.com`)
return <BaseCmp api={api.current} {...props} />
}
}
}
To use the above, we need to add the below code;
const Comp1 = withMyApi({ secure: false })(MyComponent)
// OR, if you want to use more HOCs
const composedHOC = compose(
withRouter,
withApi({ secure: false })
)
const Comp1 = composedHOC(MyComponent) // This one looks much better now
Based on the examples above, we can see that
We should not use compose with HOC's if we want to accept component as param along with one or more config object. We should instead use the nested approach.
Still better, we can use compose with HOF's and HOC's, given that the HOC's do not accept any sort of config object
However if there is need of one or more config objects, then it is better to write HOF's. In that way, consumers can use compose with our HOF.
Finally, there are a few caveats where React HOC should not be used;
Don’t use HOCs inside render method. The reconciliation process of React uses component identity to make a decision whether it should update the existing subtree or mount a new one across renders.
Thus, this component identity needs to be consistent across render. Also, when the state of a component changes, React needs to compute if it is required to update the DOM. It does this by creating virtual DOM and comparing that with the current DOM. Also, the virtual DOM would always have the updated component state. Static methods should be copied over If we want to use a static method on a React component, it is not very useful specially if we want to apply HOC on that component.
Basically, each time we apply any HOC to a base component, it would return us a new enhanced component. Thus, the new component does not contain any of the static methods of the base component.
To resolve this issue, we can make use of the "hoist-non-react-statics" package which would automatically copy all non-React static methods. Refs are not sent to the base component We need to look closely at refs, since they do not get passed through.
The reason for this is ref is not technically a prop - like a key. It is handled specially by React. To fix this issue, we need to use the React.forwardRef API.
Chris M.
I love doing frontend development and am a fan of cutting edge frameworks like React, Angular, Backbone, etc
See other articles by Chris
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!