Before the introduction of contexts and hooks in React v16.8, Higher-Order Components (or HOCs for short) were a common sight. Today, it is an under-used pattern.

HOCs are Wrappers

A Higher-Order Component is a play on the functional programming concept of a High-Order Function. It is a function that accepts a component and returns it wrapped in additional logic.

While the concept presents infinite possibilities, practical applications should be limited to transparently adding wrappers or logic. Transparently means that HOCs should avoid surprising behavior: no modifying the component or the props it receives.

Conventions

HOC conventions suggest three general rules:

  1. A HOC should not change the API of a provided component

    A prop passed from the parent should reach the underlying component. Forwardining props is easy to do by using {...props} after any additional props.

  2. HOCs should have a name following the withNoun pattern

    For example withPropsLogger or withThemeProvider.

  3. HOCs should not have any parameters aside from the Component itself. HOCs can handle additional parameters via currying.

    A consistent HOC signature is important for their composability.
    👍 Correct signature withNoun("some data")(MyComponent)
    👎 Wrong order of calls withNoun(MyComponent)("some data")
    👎 Missing currying withNoun("some data", MyComponent)

  4. (Bonus) Provide a DisplayName:

    While not a necessary development pattern, providing a custom DisplayName is a useful pattern for debugging. Adding it will make error messages and React Dev Tools more helpful in identifying issues in HOCs.

    function withSubscription(WrappedComponent) {
      class WithSubscription extends React.Component {/* ... */}
      WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`;
      return WithSubscription;
    }
    
    function getDisplayName(WrappedComponent) {
      return WrappedComponent.displayName || WrappedComponent.name || 'Component';
    }
    

HOCs are Easy

The type signature of a HOC is generic: it accepts the same props as the component it wraps.

interface HOC<T> {
  (Component: React.ComponentType<T>): (props: T) => JSX.Element
}

Some of the most common examples of HOCs involve…

Props logging:

const withPropsLogger = (Component) => (
  (props) => {
    console.log(props);
    return <Component {...props}/>
  };
);

Adding formatting, notice the currying pattern:

const withExtraPadding = (padding) => (Component) => (
  (props) => (
    <div styles={{ padding }}>
      <Component {...props}/>
    </div>
  )
);

Adding Context Providers, once more with the currying pattern:

const withMyContext = (myContextValue) => (Component) => (
  (props) => (
    <MyContext.Provider value={myContextValue}>
      <Component {...props}/>
    </MyContext.Provider>
  )
);

HOCs are Composable

Each of the HOCs above can be used on a component both individually or in combination.

const MyWrappedComponent = withPropsLogger(
  withMyContext(5)(
    withExtraPadding(20)(
      MyComponent
    )
  )
);

Combining them without functional tools makes it difficult to read. Importing reduceHOCs and applyHOCs functions helps a lot with clarity.

const MyWrappedComponent = applyHOCs(
  withPropsLogger, 
  withMyContext(5), 
  withExtraPadding(20),
)(MyComponent);

The Don’ts

Don’t use HOCs Inline

React keeps track of the component state using a combination of component object identity and its location in the React virtual DOM. Changing either the identity or the location will cause a loss of state.

export default function App() {
  // Create a function with a new identity on every render
  // which makes it lose state track of internal
  const ClickCounterWithLogger = withPropsLogger(ClickCounter);
  return (
      <ClickCounterWithLogger />
  );
}

Interactive example on CodeSandbox

Don’t use Conditional Wrappers

Similar to inline HOCs, if the tree produced by a component changes in structure, such as a wrapper removal, React will be unable to persist child states.

Don’t use HOCs for Data

Another example of situations for which HOCs are not suitable for are data providers:

function withData(Component) {
  const { data, isLoading } = useApi();
  return props => <Component 
                    data={data} 
                    isLoading={isLoading} 
                    {...props}
                  />
}

The data provider HOC pattern was formerly a common use case for HOCs in React. The highest profile example is Redux’s connect HOC, which now has a Tip at the top of the page containing the following:

connect still works and is supported in React-Redux 8.x. However, we recommend using the hooks API as the default.

At the time, it was a pattern that gathered mixed feelings and metaphorical discussions on whether “tails should wag dogs." Hooks have superceded this practice.

Don’t be Clever

Armed with a refreshed interest in HOCs, remember that HOCs are a tool to lessen the mental load of JSX. They are not an opportunity to show off your functional programming skills.

Deciding when is an appropriate time for HOCs is a judgment call<> much like determining when to use contexts, and often comes down to opinion. With that said, I am firm on a few cases when you should not use HOCs.

One such case is when HOCs implement logic that should be owned by either the component itself or its parent. For example, defaulting props:

// A parent component is responsible for supplying
// default props, or the child component should
// be handling it.
withDefaultValueForProp(MyComponent)({
    field: "data",
    defaultData: [1, 2, 3]
});

The above creates logic gaps that are difficult to modify if you end up needing something more granular. What if MyComponent is changed to either accept a data={...} or an isLoading prop? The HOC will need to go. Any HOC doing too much is destined do nothing at all.

A more dramatic case is implementing dynamic logic in the HOC call.

withPropTransformation(MyComponent)(props => {
  return {
    ...props,
    name: props.name.toUpperCase()
  } 
});

HOCs should be reserved when the logic is so minimal that you feel comfortable approaching with an “out of sight, out of mind” mentality.

Additional Information for Library Components

I received comments about this post telling me that I didn’t go deep enough into the weeds of HOCs that could potentially bite library authors. I didn’t because this post is Effective HOCs, not Everything You Need To Know About HOCs (maybe one day).

Nonetheless, I will go through some of the more common edge-cases HOC authors need to think about. By no means is the following exhaustive!

A Note on Refs

If you need to wrap a component that uses ref in a HOC, including a React.forwardRef call will make it work.

const withPropsLogger = (Component) => {
  return React.forwardRef((props, ref) => {
    console.log(props);
    return <Component {...props} ref={ref} />
  });
}

I haven’t found a method to add types to a HOC while supporting components simultaneously with and without forwardRef.

type HOCForRefComponent<T, P extends {}> = React.ForwardRefExoticComponent<React.PropsWithoutRef<P> & React.RefAttributes<T>>;
const withPropsLoggerWithRef = <T, P extends {}>(
  Component: HOCForRefComponent<T, P>
): HOCForRefComponent<T, P> => React.forwardRef<T, P>(
  (props, ref) => <Component {...props as React.PropsWithoutRef<P>} ref={ref} />
)

Another approach is avoiding the ref keyword altogether when it comes to HOCs and using a prop name like innerRef.

There are a few other edge cases discussed in the React documentation on refs and HOCs, but it focuses on class components.

A Note on Static Properties

In my eyes, static properties are an anti-pattern but I digress. In the context of HOCs, static properties are not automatically passed along.

You need explicitly pass each static property to your HOC’d component.

Addressing Comments

This post has received some traction on Reddit. I’m pretty happy about the attention, but most of the commenters are clearly not very happy about HOCs. Many believe some variation of “hooks good, HOCs bad”.

Hooks > HOCs

While I appreciate this post keeping the art of the HOC alive and I really like their dos and donts, I still think hooks are a significantly better option 99.9% of the time. HOCs a very easy way to add surprising behavior to components that might confound future developers. Hooks basically killed them because they are a more predictable, explicit pattern. – /u/sickcodebruh420

I agree: hooks did kill HOCs but only because HOCs used to be the only way of providing app-level data without prop-drilling. Data-provider HOCs were “99.9%" of them and seemingly traumatized developers to the point that they never want to see a HOC again.

Good HOCs exist, and provide powerful patterns which can help write clear and succinct expressions in lieu of JSX.

const withCommon = reduceHOCs(
  withErrorBoundary, 
  withThemeProvider, 
);

const withGlobal = reduceHOCs(
  withQueryClientProvider, 
  withStoreProvider,
);

applyHOCs(
  withCommon, 
  withGlobal,
)(App);

applyHOCs(
  withCommon
)(Page)

(Not Quite) Everything About HOCs

Another comment suggested that I didn’t cover HOCs thoroughly enough and brought up a few points:

Just wanted to add my two cents if somebody is making library that is built around HOC (not sure why?) is to ensure these things:
- HOC must pass static properties (use hoist-non-react-statics)
- HOC must have two versions: one that passes ref and other that is not.
- It would be nice if HOC would set nice displayName
/u/Reeywhaar

The post has been updated to reflect these points. I suggest reading the whole thread with /u/Reeywhaar.