Since the React hooks API was introduced, a lot of questions have risen about whether or not React hooks will replace other common libraries and patterns in the React+Redux ecosystem.

Hooks were designed to replace class and provide another great alternative to compose behavior into your components. Higher Order Components are also useful for composing behavior. There is some obvious overlap, so do React hooks replace Higher Order Components? It’s pretty clear that they can replace some HOCs. But should you replace all your HOCs with React hooks?

To figure that out, we should first have a good understanding of what Higher Order Components are, how they’re used, and some of the caveats you should watch out for when using them.

What Are Higher Order Components?

A Higher Order Component (HOC) is a component that takes a component and returns a component. HOCs are composable using point-free, declarative function composition. Here’s an example that will log every page view to a /logger API:

import React, { useEffect } from 'react'; const withLogging = Component => props => {

useEffect(() => {

fetch(`/logger?location=${ window.location}`);

}, []); return <Component {...props } />;

}; export default withLogging;

To use it, you can mix it into an HOC that you’ll wrap around every page:

import compose from 'ramda'; import withRedux from './with-redux.js';

import withAuth from './with-auth.js';

import withLogging from './with-logging.js';

import withLayout from './with-layout.js'; const page = compose(

withRedux,

withAuth,

withLogging,

withLayout('default'),

); export default page;

This creates a component hierarchy you can think of like this:

<withRedux>

<withAuth>

<withLogging>

<withLayout>

<MyPageComponent />

</withLayout>

</withLogging>

</withAuth>

</withRedux>

To use this for a page:

import page from '../hocs/page.js';

import MyPageComponent from './my-page-component.js'; export default page(MyPageComponent);

This is a great pattern if:

The HOC does not need to create more than one prop to pass to child components. It’s preferable if they don’t create any props at all.

to pass to child components. It’s preferable if they don’t create any props at all. The HOC does not create implicit dependencies that other HOCs or components rely on.

that other HOCs or components rely on. All or many components in your app will need to share the behavior.

Note: These are not ironclad rules from which you should never deviate. Instead, they are rules-of-thumb that will usually serve you well. I frequently make a small exception to the “no implicit dependencies” rule-of-thumb for the HOC that supplies my Redux provider. I call that one withRedux . Once Redux is connected, other HOCs can access the state to authorize users, etc.

The pattern of composing HOCs for functionality shared by all your pages is still the best approach I’m aware of for lots of cross cutting concerns, such as shared layout, logging, authentication/authorization, and anything else that is shared but doesn’t need any component-specific logic.

Why Use HOCs?

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. Unlike hooks, HOCs can be composed declaratively using standard function composition, e.g.:

const page = compose(

withRedux,

withAuth,

withLogging,

withLayout('default'),

);

If you completely eliminate HOCs, other options (e.g., hooks and render props) require ad-hoc composition requiring lots of code duplication and lots of one-off implementations of the same logic spread all over your app and added to components that should not be concerned with them, violating some fundamental software design principles, including:

Keep the following in mind when you use HOCs because when used in the wrong way, HOCs can create problems:

Moving around the order of HOCs can break things.

Passed props are implicit dependencies. It can get confusing to understand where props are coming from vs directly importing the behavior you depend on in the components that use it.

It can get confusing to understand where props are coming from vs directly importing the behavior you depend on in the components that use it. Using lots of HOCs with lots of props can cause prop collisions — multiple HOCs competing to supply the same prop name to your components.

Note: HOCs are composable function components which can mix anything into the props passed to the wrapped component, making them a form of functional mixin when they mix in props. All the warnings that apply to functional mixins apply to HOCs that mix in props.

Hooks move those implicit dependencies into each individual component, so you can see them in the component and know where all your dependencies come from. Prop collisions are avoided because you can assign the hook return values to any variable you want, and explicitly pass them to child dependencies as props, manually dealing with name collisions as needed.

Here’s an example of a real component that uses hooks:



import t from 'prop-types';

import TextField, { Input } from ' import React, { useState } from 'react';import t from 'prop-types';import TextField, { Input } from ' @material/react-text-field '; const noop = () => {}; const Holder = ({

itemPrice = 175,

name = '',

email = '',

id = '',

removeHolder = noop,

showRemoveButton = false,

}) => {

const [nameInput, setName] = useState(name);

const [emailInput, setEmail] = useState(email); const setter = set => e => {

const { target } = e;

const { value } = target;

set(value);

}; return (

<div className="row">

<div className="holder">

<div className="holder-name">

<TextField label="Name">

<Input value={nameInput} onChange={setter(setName)} required />

</TextField>

</div>

<div className="holder-email">

<TextField label="Email">

<Input

value={emailInput}

onChange={setter(setEmail)}

type="email"

required

/>

</TextField>

</div>

{showRemoveButton && (

<button

className="remove-holder"

aria-label="Remove membership"

onClick={e => {

e.preventDefault();

removeHolder(id);

}}

>

×

</button>

)}

</div>

<div className="line-item-price">${itemPrice}</div>

<style jsx>{cssHere}</style>

</div>

);

};

Holder.propTypes = {

name: t.string,

email: t.string,

itemPrice: t.number,

id: t.string,

removeHolder: t.func,

showRemoveButton: t.bool,

}; export default Holder;

This code uses useState to keep track of the ephemeral form input states for name and email:

const [nameInput, setName] = useState(name);

const [emailInput, setEmail] = useState(email);

This state is only used for this component, so hooks are a good fit for this use-case.

The disadvantage to hooks is that if you use them incorrectly, you could spread logic that could be self contained in one place across all the components in your app, opening up lots of opportunities to forget things or to spread duplicated bugs into a thousand places in your app instead of just one.

Instead of banning all HOCs, you should be aware of which problems are a good use-case for HOCs and which aren’t.

You may have a poor use-case for HOCs if:

The behavior requires adding a bunch of props to a component.

The behavior is only used in one component.

The behavior must be customized for each component which uses it.

You may have a good use-case for HOCs if:

The behavior is not specific to any single component, but rather applies to many or all components in the app, and

but rather and The behavior doesn’t need to provide a bunch of props to the components that use it.

to the components that use it. Components can be used stand-alone without the behavior from the HOC.

without the behavior from the HOC. No custom logic needs to be added to a component being wrapped by the HOC.

For broadly-used cross-cutting concerns, using HOCs will give you a very simple, declarative, point-free implementation located in a single place, while using hooks will give you a lot of ad-hoc imperative implementations that could add a significant amount of code and complexity throughout the UI code in your app.