# 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.

- **Source:** DevDreaming (https://devdreaming.com)
- **Canonical URL:** https://devdreaming.com/blogs/higher-order-component-hoc-react
- **Author:** CodeBucks
- **Published:** 2021-02-02
- **Last updated:** 2026-04-28
- **Topics:** React js

---

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. 👇

[▶ Watch the video on YouTube](https://www.youtube.com/watch?v=kdaqsbEu0bQ)

---

### 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](https://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:

| Situation | Reach for... | Reason |
| --- | --- | --- |
| Shared stateful logic (data fetching, timers, form state) | Custom hook | Composes cleanly, no extra tree node |
| Shared side effects (subscriptions, analytics) | Custom hook | `useEffect` inside a hook is idiomatic |
| Auth / permission gate -- render nothing or redirect | HOC | Wraps any component; gate is structural |
| Error boundary around a third-party component | HOC | No hook equivalent for `componentDidCatch` |
| Injecting behavior into a component you can't modify | HOC | You control the wrapper, not the source |
| Consuming a library HOC (`connect`, `observer`) | HOC (it's given) | That's the library's API |
| Adding a `displayName` or dev-tools label | HOC | Structural 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](https://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`:

```javascript
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`:

```javascript
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:

```javascript
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`:

```javascript
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.

```javascript
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:

| HOC | Library | What it does |
| --- | --- | --- |
| `connect(mapState, mapDispatch)(MyComponent)` | `react-redux` | Subscribes to the Redux store, injects state and dispatch as props |
| `observer(MyComponent)` | `mobx-react` | Re-renders when any observed MobX observable accessed in render changes |
| `withErrorBoundary(MyComponent, { fallback })` | `@sentry/react` | Wraps with an error boundary that reports to Sentry |
| `withRouter(MyComponent)` | React Router v5 (legacy) | Injected `history`, `location`, `match` -- **replaced by **`useNavigate`** / **`useLocation`** hooks in v6** |
| `createFragmentContainer(MyComponent, fragment)` | Relay | Injects 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](https://legacy.reactjs.org/docs/higher-order-components.html) and are easy to hit silently.

**1. Don't mutate the wrapped component.**

```javascript
// 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`:

```javascript
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`:

```javascript
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.**

```javascript
// 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:**

```javascript
// 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):**

```javascript
// 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.

```typescript
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:

```javascript
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](https://github.com/codebucks27/React-Tutorials/tree/main/src/components/HOC).

If you have questions, drop them in the comments. Happy coding! 😄

---

## Related on DevDreaming

- [All Blog Posts](https://devdreaming.com/blogs)
- [Free Developer Tools](https://devdreaming.com/tools)
- [Video Tutorials](https://devdreaming.com/videos)
- [AI Tools for Developers](https://devdreaming.com/ai-tools)

---

_This is the Markdown twin of a page on **DevDreaming** -- free developer tutorials, tools, and AI resources. Source of truth: the canonical HTML URL above._