Feeling stressed? I use Miracle of Mind daily.Try it now!

Higher-Order Components (HOC) in React: When to Use Them in 2026

A practical guide to Higher-Order Components (HOC) in React for 2026. Learn what HOCs are, when to reach for one vs a custom hook, common pitfalls to avoid, how to type them in TypeScript, and which popular libraries still ship them today.

CodeBucks

CodeBucks

13 min read0 views
Higher-Order Components (HOC) in React: When to Use Them in 2026

Hi there πŸ‘‹

You've probably heard "HOCs are dead β€” just use hooks." That's half true and half not. Redux's connect, MobX's observer, and Sentry's withErrorBoundary are HOCs that ship in production apps every single day in 2026. The pattern isn't gone β€” it just moved mostly into library code. Knowing when to use one (and when not to) is what separates a confident React developer from someone who cargo-cults patterns.

If you'd rather watch than read, the video below covers the core concept and the counter example step by step. πŸ‘‡


Why Are We Still Talking About HOCs in 2026?

React 16.8 shipped hooks in February 2019. By 2021, most "learn HOCs" tutorials were already showing class-component examples that felt dated. By 2026 the community consensus is clear: for new app-level code, a custom hook is almost always the better tool. Custom hooks compose more naturally, don't introduce an extra component into the tree, and don't have the prop-collision issues HOCs are known for.

But three things keep HOCs relevant:

  1. Library APIs. react-redux's connect, MobX's observer, and Relay's createFragmentContainer are all HOCs. You consume them whether you prefer them or not.
  2. Error boundaries. React's error boundary API (componentDidCatch) requires a class component. Sentry's withErrorBoundary and similar wrappers are HOCs precisely because there's no hook equivalent for catching render errors yet.
  3. Wrapping third-party components you can't modify. If you need to inject behavior into a component whose source you don't own, an HOC is often the cleanest option.

The takeaway: learn HOCs so you can read and debug the ones in your node_modules, and know the handful of situations where writing one yourself still makes sense.


What Is a Higher-Order Component?

The official React legacy docs define it this way:

"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."β€” legacy.reactjs.org/docs/higher-order-components.html

In plain terms: an HOC is a function that takes a component and returns a new, enhanced component.

HOC = (WrappedComponent) => EnhancedComponent

It mirrors higher-order functions in JavaScript β€” Array.map takes a function and returns a new array; an HOC takes a component and returns a new component. Same idea, different domain.

The classic problem HOCs solve is shared cross-cutting logic across multiple unrelated components. Say LikesCount and CommentsCount both need the same counter behaviour β€” initialise from a value, increment on click. Without an HOC you copy that logic twice. With an HOC you write it once and wrap both.


When to Reach for an HOC vs a Hook

This is the question nobody asked in 2021. Here's the decision matrix:

SituationReach for…Reason
Shared stateful logic (data fetching, timers, form state)Custom hookComposes cleanly, no extra tree node
Shared side effects (subscriptions, analytics)Custom hookuseEffect
Auth / permission gate β€” render nothing or redirectHOCWraps any component; gate is structural
Error boundary around a third-party componentHOCNo hook equivalent for
Injecting behavior into a component you can't modifyHOCYou control the wrapper, not the source
Consuming a library HOC (HOC (it's given)That's the library's API
Adding a HOCStructural concern, not logic

The React docs' custom hooks page puts it well:

"Custom Hooks let you share stateful logic but not state itself. Each call to a Hook is completely independent from every other call to the same Hook."β€” react.dev/learn/reusing-logic-with-custom-hooks

That independence is both a feature and a constraint. An HOC shares the instance β€” it wraps a specific component in a specific tree position. A hook runs fresh logic per call site. Neither is universally better; they model different things.


Building a Counter HOC: Class Version

Let's build the example from scratch β€” two components (LikesCount and CommentsCount) that share counter logic. First, the class-based HOC. Create a file called Hoc.js:

import React, { Component } from "react";

const HOC = (WrappedComponent, data) => {

  return class extends React.Component {

    constructor(props) {
      super(props);
      this.state = {
        count: data,
      };
    }

    handleClick = () => {
      this.setState({
        count: this.state.count + 1,
      });
    };

    render() {
      return (
        <WrappedComponent
          CountNumber={this.state.count}
          handleCLick={this.handleClick}
        />
      );
    }

  };
};

export default HOC;

Line by line:

  • Line 3: HOC is a function that accepts two arguments β€” the WrappedComponent we want to enhance, and data (the starting count value).
  • Line 5: The HOC returns an anonymous class component. This is the "enhanced" component React will actually render.
  • Lines 8–12: We initialise count in local state, seeding it from the data argument.
  • Lines 14–18: handleClick increments the counter by 1 each time it fires.
  • Lines 20–27: The render method passes count and handleClick down to WrappedComponent as props. Importantly, {...this.props} should also be spread here so any props the parent passes through aren't swallowed β€” we'll fix that in the functional version.

Now create LikesCount.js:

import React, { Component } from "react";
import HOC from "./HOC";

class LikesCount extends Component {
  render() {
    return (
      <div>
        {this.props.CountNumber} <br />
        <button onClick={this.props.handleCLick}>Like πŸ‘πŸ»</button>
      </div>
    );
  }
}

const EnhancedLikes = HOC(LikesCount, 5);

export default EnhancedLikes;
  • Line 8: Renders the current count received via props.
  • Line 9: The button fires handleCLick (note: the prop name typo here comes from the HOC β€” we'll fix it in the functional version).
  • Line 15: HOC(LikesCount, 5) β€” wrap the component and seed the counter at 5 (representing 5 existing likes). The returned EnhancedLikes is what we export and render.
In plain terms: HOC took LikesCount and data, then returned an EnhancedLikes component with counter logic already baked in.

CommentsCount.js works identically β€” just different text and a different starting value:

import React, { Component } from "react";
import HOC from "./HOC";

class CommentsCount extends Component {
  render() {
    return (
      <div>
        Total Comments: {this.props.CountNumber} <br />
        <button onClick={this.props.handleCLick}>Add Comment!</button>
      </div>
    );
  }
}

const EnhancedComments = HOC(CommentsCount, 10);

export default EnhancedComments;
  • Line 15: Seeded at 10, so the counter starts from 10 existing comments.

Use both in App.js:

import React from "react";
import EnhancedLikes from "./components/HOC/LikesCount";
import EnhancedComments from "./components/HOC/CommentsCount";

function App() {
  return (
    <div className="App">
      <EnhancedLikes />
      <EnhancedComments />
    </div>
  );
}

export default App;

One piece of counter logic. Two components benefit from it. Zero duplication. πŸ˜„


The Functional / Hooks-Friendly HOC Pattern

The class version works, but the modern default is a functional HOC. It's shorter, it handles prop pass-through correctly, and it integrates with hooks naturally. This is the pattern you should use for any new code.

import React, { useState } from "react";

const hoc = (WrappedComponent, data) => {

  // Give the returned component a name for React DevTools
  function HOC(props) {
    const [count, setCount] = useState(data);

    const handleClick = () => {
      setCount((prev) => prev + 1);
    };

    return (
      <WrappedComponent
        countNumber={count}
        handleClick={handleClick}
        {...props}
      />
    );
  }

  HOC.displayName = `WithCounter(${
    WrappedComponent.displayName || WrappedComponent.name || "Component"
  })`;

  return HOC;
};

export default hoc;

Three improvements over the class version:

  • Line 7: useState(data) β€” same initialisation, no constructor boilerplate.
  • Line 10: Functional updater (prev) => prev + 1 is safer than reading count directly inside the callback (avoids stale closure bugs).
  • Line 15: {...props} spreads all props the parent passes down β€” the class version above was missing this, which would silently swallow props.
  • Lines 21–24: Setting displayName means React DevTools will show WithCounter(LikesCount) instead of HOC. This is not optional if you want debuggable code.

Real-World HOCs You're Probably Already Using

These ship in popular libraries and work exactly like the pattern above β€” a function that wraps your component and injects extra props or behavior:

HOCLibraryWhat it does
connect(mapState, mapDispatch)(MyComponent)react-reduxSubscribes to the Redux store, injects state and dispatch as props
observer(MyComponent)mobx-reactRe-renders when any observed MobX observable accessed in render changes
withErrorBoundary(MyComponent, { fallback })@sentry/reactWraps with an error boundary that reports to Sentry
withRouter(MyComponent)React Router v5 (legacy)Injected
createFragmentContainer(MyComponent, fragment)RelayInjects GraphQL fragment data; still an HOC as of Relay 16

The React Router case is instructive: withRouter was an HOC in v5 and was removed in v6 specifically because hooks (useNavigate, useLocation, useParams) cover the same ground more cleanly. That's the general migration direction β€” hooks replace app-level HOCs, but library-level HOCs remain.


Common Pitfalls

These come from the official React HOC documentation and are easy to hit silently.

1. Don't mutate the wrapped component.

// Bad β€” modifies the original component's prototype
function withLogging(WrappedComponent) {
  WrappedComponent.prototype.componentDidUpdate = function (prevProps) {
    console.log("prev props:", prevProps);
    console.log("next props:", this.props);
  };
  return WrappedComponent;
}

Mutation creates a leaky abstraction. If another HOC also wraps this component, it will overwrite your prototype change. Always return a new component.

2. Copy static methods.

Static methods don't transfer automatically when you wrap a component. Use hoist-non-react-statics:

import hoistNonReactStatics from "hoist-non-react-statics";

function withCounter(WrappedComponent, data) {
  function HOC(props) { /* ... */ }
  hoistNonReactStatics(HOC, WrappedComponent);
  return HOC;
}

Without this, any static getDerivedStateFromProps or custom static methods on WrappedComponent are silently lost.

3. Forward refs.

Refs don't pass through HOCs like props do. Use React.forwardRef:

function withCounter(WrappedComponent, data) {
  function HOC({ forwardedRef, ...props }) {
    const [count, setCount] = useState(data);
    return (
      <WrappedComponent
        ref={forwardedRef}
        countNumber={count}
        handleClick={() => setCount((p) => p + 1)}
        {...props}
      />
    );
  }

  HOC.displayName = `WithCounter(${WrappedComponent.displayName || WrappedComponent.name})`;

  return React.forwardRef((props, ref) => (
    <HOC forwardedRef={ref} {...props} />
  ));
}

4. Don't apply HOCs inside render.

// Bad β€” creates a new component class on every render, blowing away state
function MyComponent() {
  const EnhancedFoo = withCounter(Foo, 0); // ← inside render!
  return <EnhancedFoo />;
}

// Good β€” apply once at module level
const EnhancedFoo = withCounter(Foo, 0);

function MyComponent() {
  return <EnhancedFoo />;
}

5. Pass all props through.

Always spread {...props} onto the wrapped component. Swallowing props is the most common HOC bug and the hardest to debug.


Migrating an HOC to a Custom Hook

Here's the same counter logic β€” first as an HOC, then as a custom hook β€” so you can see exactly what changes.

HOC version:

// hoc/withCounter.js
function withCounter(WrappedComponent, initialCount) {
  function HOC(props) {
    const [count, setCount] = useState(initialCount);
    return (
      <WrappedComponent
        countNumber={count}
        handleClick={() => setCount((p) => p + 1)}
        {...props}
      />
    );
  }
  HOC.displayName = `WithCounter(${WrappedComponent.name})`;
  return HOC;
}

// Usage
const EnhancedLikes = withCounter(LikesCount, 5);

Custom hook version (preferred for new code):

// hooks/useCounter.js
function useCounter(initialCount) {
  const [count, setCount] = useState(initialCount);
  const handleClick = () => setCount((p) => p + 1);
  return { count, handleClick };
}

// Usage β€” no wrapper component in the tree
function LikesCount() {
  const { count, handleClick } = useCounter(5);
  return (
    <div>
      {count} <br />
      <button onClick={handleClick}>Like πŸ‘πŸ»</button>
    </div>
  );
}

The hook version is shorter, there's no extra component in the React tree, and there's no prop-naming collision risk. The HOC version is still valid code β€” it's just not the first tool you'd reach for when starting fresh.


TypeScript: Typing an HOC Correctly

Typing an HOC in TypeScript is one of those things that looks scary until you see the pattern once.

import React, { useState, ComponentType } from "react";

// The props the HOC injects into WrappedComponent
interface WithCounterProps {
  countNumber: number;
  handleClick: () => void;
}

// P = the WrappedComponent's own props (minus what HOC injects)
function withCounter<P extends object>(
  WrappedComponent: ComponentType<P & WithCounterProps>,
  initialCount: number
) {
  function HOC(props: P) {
    const [count, setCount] = useState(initialCount);
    const handleClick = () => setCount((prev) => prev + 1);

    return (
      <WrappedComponent
        {...props}
        countNumber={count}
        handleClick={handleClick}
      />
    );
  }

  HOC.displayName = `WithCounter(${
    WrappedComponent.displayName || WrappedComponent.name || "Component"
  })`;

  return HOC;
}

export default withCounter;

Key things happening here:

  • P extends object β€” generic constraint that captures WrappedComponent's own props.
  • ComponentType<P & WithCounterProps> β€” tells TypeScript that WrappedComponent expects both its own props and the injected counter props.
  • HOC(props: P) β€” the returned component accepts only P (the caller's props). TypeScript knows countNumber and handleClick come from the HOC, not the caller.

FAQ

Are HOCs deprecated in React 19?

No. HOCs are not part of the React API β€” they're a pattern β€” so they can't be deprecated. React 19 didn't change this. The React team recommends hooks for new app-level code, but HOCs remain valid and are still used widely in the ecosystem. The legacy docs page at legacy.reactjs.org/docs/higher-order-components.html remains available and hasn't been marked deprecated.

HOC vs render props vs hooks β€” which wins?

Hooks win for most app-level code in 2026. Render props (the <Consumer>{value => ...}</Consumer> pattern) solved the same problems as HOCs but avoided prop-collision issues; hooks then made both patterns mostly unnecessary for new code. Choose HOCs when you need structural wrapping (error boundaries, route protection) or when consuming a library that ships HOC-style APIs. Use hooks for everything else.

Can I use HOCs with React Server Components?

Only partially. Server Components can't use hooks, state, or effects β€” and since most HOCs use at least one of those, a typical HOC will force the wrapped component into a Client Component. If you need cross-cutting logic in Server Components, use composition (pass children, pass data through props) rather than HOCs.

Why is the wrapped component showing as Unknown in React DevTools?

You forgot to set displayName. Add this line inside your HOC factory:

HOC.displayName = `WithCounter(${WrappedComponent.displayName || WrappedComponent.name || "Component"})`;

What's the hoist-non-react-statics library for?

When you wrap a component in an HOC, static methods defined on the original component don't automatically copy over to the wrapper. hoist-non-react-statics (npm) copies all non-React static methods in one line: hoistNonReactStatics(HOC, WrappedComponent). Skip it and you'll silently lose things like getLayout, custom defaultProps overrides, or any static method your team defined.

How do I type an HOC that forwards refs in TypeScript?

Combine React.forwardRef with a generic. The pattern is verbose but the type safety is worth it β€” it prevents the common bug where consumers pass a ref and nothing happens because the HOC swallowed it.


Conclusion

Higher-Order Components are a foundational React pattern that's alive, used in production, and worth understanding β€” even in 2026 when hooks handle most of the same use cases more cleanly. The short version:

  • HOCs = structural wrapping, library APIs, error boundaries, and components you can't modify
  • Hooks = stateful logic, effects, derived data, and anything new you're building from scratch

The counter example above is deliberately simple so the pattern is clear. Real-world HOCs follow the exact same shape: a function that takes a component, adds something to it, and returns a new component. Once you internalize that, reading connect from react-redux or observer from MobX becomes straightforward.

You can find the full code for the examples in this post here: HOC in React β€” GitHub.

If you have questions, drop them in the comments. Happy coding! πŸ˜„

Related Posts