Higher-Order Components Are Misunderstood In React
When you interview 100s of "senior" React developers, you'd be surprised how many fail to answer this simple question:
“What is a higher-order component?”
And even less can answer the follow-up question:
“Why do higher-order components in React exist?”
In other words:
“Did any React team member consciously create higher-order components as a concept and put them into React?”
In this article, you're going to find out the correct answers to these questions. And you will learn everything you need to know about HOCs.
Note: Make sure you are understanding arrow functions and the basics of React.
Abstract
You're first going to see the formal definition of HOCs and through the rest of this article you're going to understand the theory behind it.
A Higher-Order component is a function that takes a component and returns a new component.
The React docs further state:
"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."
The theory behind HOCs comes from ...
Function Composition
In mathematics, function composition is the act of combining functions to form a new function or a result, by applying one function to the result of another.
In JavaScript, this looks this:
Notice how you assign the combined functions to a new variable called doubleThenInc
, which you can do because JavaScript has first-class functions.
You can learn more about first-class functions in this article, which also explains the difference between useCallback
and useMemo
.
A programming language has first-class functions if it allows you to assign functions to variables.
You can abstract the composition to combine any two functions:
You omit the argument x
in the definition of doubleThenInc2
. This means doubleThenInc2
is defined point-free, which is when you define a function without mentioning its arguments.
If you want to compose an arbitrary amount of functions, you need to generalize the composition function.
More sophisticated versions of the compose
function are frequently exposed by libraries that leverage HOCs such as Redux and Apollo.
The arguments and return values of functions have to line up to compose them. For example, you can't compose a function that accepts an object and returns a string with a function that receives an array and returns a number.
Since inc
and double
both take and return numbers, you can compose them in any order.
Additionally, compose2
and compose
are higher-order functions.
A higher-order function is a function that either receives or returns a function or does both.
multiply
IS a higher-order function because it takes in a number and returns a function.double
IS NOT a higher-order function because it neither receives nor returns a function. It is defined point-free.map
IS a higher-order function because it both accepts and returns a function.doubleMap
IS NOT a higher-order function because it neither receives nor returns a function. It is defined point-free.
React components can either be functions or classes.
In JavaScript, the class
keyword is essentially a wrapper for the function
keyword and handles prototypal inheritance. In other words, classes compile to constructor functions.
Therefore, since all components are functions in React and JavaScript has higher-order functions, you get HOCs for free. That is what the docs mean when they say HOCs "are a pattern that emerges from React’s compositional nature."
Now you should understand the basic definition of HOCs:
A Higher-Order component is a function that takes a component and returns a new component.
Any function whose input and output is a React component is a HOC.
HOCs by Example
You probably want to see what a higher-order component looks like. Follow the rest of this tutorial to write your own using TDD. You're going to use Vitest with React Testing Library to write the tests.
You can deduce two requirements from the definition of a higher-order component:
- HOCs are functions.
- HOCs take a component and return a component.
You can capture these requirements in a unit test.
The test checks both requirements because when this test passes, you can logically deduce that your HOC is a function and that it returns a component without spelling out those requirements explicitly. If the HOC were not a function and you tried to call it, it would throw, and your unit test would fail with a clear stack trace. Likewise, the test renders the return value of the HOC, which ensures it is a React component.
Notice how you did NOT test for typeof function
here. Unit tests which only test types are an anti-pattern. It's redundant with simply calling the function and checking its output value. In general, type checks are redundant with well-written unit tests. This is why unit tests can catch most type errors, without the need for additional measures like type annotations (though annotations and type inference can still be useful to enable IDE tooling).
You can get the test to pass by making your HOC the identity function.
Your test result should now look like this.
Why HOCs?
Your current HOC does nothing. And you're going to change that, soon.
In general, HOCs excel at abstracting logic or styling. They allow you to avoid unnecessary code duplication. If you find yourself repeating certain JSX or logic patterns in your component, you might be able to abstract them away using HOCs.
For example, if you have a page for your web site or a screen for your React Native app, most pages or screens have the same layout. They all share elements such as headers, footers or formatting containers.
Making Your HOC Useful
You can add styling abilities to our HOC and call it withLayout
instead of MyHOC
.
Start by adding a test that verifies that your HOC adds a layout to your component.
Watch your test fail, then create a layout component.
Layouts can vary depending on the app and framework that you're using. In React Native app, you find yourself writing similar layout HOCs using React Navigation's <SafeAreaView />
. In a Remix app, you won't need a layout HOC because you can export a layout component from your root.tsx
file.
Now, make your test pass by using the Layout
component in your HOC.
Your tests should both pass now.
Notice how the withLayout
HOC now takes in a component and then returns a function because before this change it actually was NOT a higher-order component.
What Most Developers Get Wrong
This also shows the most common misconception about HOCs. Many developers answer the question of "What is a higher-order component" with "it's a component that takes in a React component and returns it".
They probably think of something like this.
What you see above is a React component that takes in another React component as a prop.
But that's is NOT a higher-order component because HOCs are functions and NOT components. You can NOT render a HOC.
Looking back at the your withLayout
HOC, it contains a bug. Can you spot it?
If not, that is okay. You can write the following test to expose the error.
The new test fails.
The test exposes the problem: You fail to pass props
to the wrapped component. You can make the test pass by passing on the props the HOC receives.
Now your tests pass because your HOC correctly passes on the props to the wrapped component.
However, the abstraction capabilities of HOCs wouldn't be as useful if they didn't have another key feature. Eric Elliott describes it like this:
"The primary benefit of HOCs is not what they enable (there are other ways to do it); it's how they compose together at the page root level."
In other words, the key to using HOCs well is to know how and when you want to compose them. You you write a test to demonstrate the "how". Spoiler: it is fundamentally function composition.
Here is a test that shows how you compose HOCs.
This test already passes.
You compose withLayout
with withTitle
. withTitle
is a HOC that injects a title
prop to a component.
Configuring HOCs
It is common for HOCs to accept configuration objects. You probably encounter this when using React Redux' connect
with mapStateToProps
. (In fact, it accepts two more arguments: mapDispatchToProps
and mergeProps
.)
Assume that some pages should render without the header, so you modify your layout component to take in a prop that let's you show and hide the header.
Now write a test that allows you to modify your HOC. You'll also need to modify your existing tests to accommodate the fact that your HOC now takes in a configuration object.
Watch all your tests fail because your component still lacks the configuration object. Add it to make them pass.
To answer the question of when to use composition for HOCs, remember what I told you learned earlier. HOCs are excellent if you want to abstract away common logic between many components. You chose to give your function a layout functionality because that is one area that most screens of your application will share. Using compose
you can define a HOC that you can use to wrap all your pages with.
Real-World Example
Here is a real-world example of a SignInForm
container component. See if you understand it, then read the explanation to check if you were correct.
In the example above, you composed 3 different HOCs.
withRouter
is a HOC from React Router DOM. It injects thehistory
object, which you can use to navigate to the password reset screen, when the user clicks the "Forgot Password" button.connect
is a HOC from React Redux. You use it to connect your component to your Redux store. You inject theloading
prop and thesignIn
action creator.withFormik
is a HOC from Formik. Formik let's you control local form state and handles form validation for you.
Sometimes you need to copy over static properties such as propTypes
, defaultProps
and getStaticProps
(if you are using Next.js) from the inner component to the resulting component. Here is a Higher-Order HOC (a function that returns a HOC), which does this for you.
BTW: When using HOCs you need to treat ref
s special, too. If you need to pass ref
s through a component hierarchy, you should probably be using a hook for the ref
instead of a HOC.
HOC Composition
You know from function composition that you can only compose functions whose types line up. Similarly, you need to pay attention to the order in which you compose your HOCs. One HOC can inject props that another might depend on. If the one that depends on the props gets injected before the prop injecting HOC, your app might break.
If you switch the order of HOCs in the real-world example above, it will break, too. withFormik(formikConfig)
depends on signIn
being defined, and transformProps
depends on both history
and the formikBag
props.
HOCs with implicit dependencies on each other may be a code smell. In some cases, it may be better to make those dependencies explicit, by importing the shared functionality into the components that need them, or taking the dependency as a configuration parameter of the HOC. It's probably ok to implicitly depend on something that's pretty universal to all your pages, such as your store provider.