React

Higher-Order Components and Why Hooks Replaced Them

What higher-order components are, how they worked in class-based React, and why hooks are a better solution for the same problems.

If you worked with React before 2019, you probably wrote higher-order components (HOCs) regularly. If you started after hooks landed, you might have run into one in an older codebase or library and wondered what it was doing.

An HOC is a function that takes a component and returns a new component with extra props or behaviour. Its signature looks like this:

function withExtraProp(WrappedComponent: React.ComponentType) {
  return function EnhancedComponent(props: any) {
    return <WrappedComponent extraProp="hello" {...props} />
  }
}

For years this was the standard way to share stateful logic between components. Then hooks arrived and solved the same problems with less indirection. I want to show you how HOCs used to work and why hooks are the better approach for almost every case.

What HOCs Solved

Before hooks, class components were the only way to hold state or run side effects. If two classes needed the same behaviour (tracking window size, subscribing to a data source, managing form state), you had two options: copy the code or wrap it in an HOC.

Here is an HOC that provides the current window width:

function withWindowSize(WrappedComponent: React.ComponentType<{ windowWidth: number }>) {
  return class extends React.Component<any, { windowWidth: number }> {
    state = { windowWidth: window.innerWidth }

    handleResize = () => {
      this.setState({ windowWidth: window.innerWidth })
    }

    componentDidMount() {
      window.addEventListener('resize', this.handleResize)
    }

    componentWillUnmount() {
      window.removeEventListener('resize', this.handleResize)
    }

    render() {
      return <WrappedComponent windowWidth={this.state.windowWidth} {...this.props} />
    }
  }
}

You would use it like this:

function MyComponent({ windowWidth }: { windowWidth: number }) {
  return <div>Window width: {windowWidth}</div>
}

const MyComponentWithWindowSize = withWindowSize(MyComponent)

The inner component did not know it was wrapped. It just received windowWidth as a prop. The HOC handled the subscription and cleanup.

The Problems with HOCs

The pattern works, but it has friction points that become annoying at scale.

Prop name collisions. If two HOCs inject a prop with the same name, the last one applied wins silently. There is no error. Your component just gets the wrong value.

// If both withUser and withTheme inject a "data" prop, one overwrites the other
const EnhancedComponent = withUser(withTheme(MyComponent))

Wrapper hell in devtools. Every HOC adds another layer of wrapper components. Compose three or four HOCs and your React devtools tree becomes a pyramid of withSomething(EnhancedComponent). Debugging means clicking through layers of anonymous wrappers to find the actual component.

Static property loss. HOCs wrap the outer component around the inner one. Static methods on the wrapped component do not carry through automatically. You had to copy them manually or use a helper like hoist-non-react-statics.

TypeScript was awkward. Typing an HOC generically was possible but verbose. The inferred types through nested HOCs often broke, and you ended up with any in places you did not want it.

Hooks Solve All of This

Hooks let you extract stateful logic into custom hooks that components call directly. No wrappers, no prop injection, no collision risk.

The same window-size logic as a hook:

function useWindowSize() {
  const [windowWidth, setWindowWidth] = useState(
    typeof window !== 'undefined' ? window.innerWidth : 0
  )

  useEffect(() => {
    const handleResize = () => setWindowWidth(window.innerWidth)
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return windowWidth
}

And the component uses it directly:

function MyComponent() {
  const windowWidth = useWindowSize()
  return <div>Window width: {windowWidth}</div>
}

Compare the two approaches:

  • No prop injection. The hook returns a value. The component decides what to call it, so collision is impossible.
  • No wrapper layers. Devtools show MyComponent, not withWindowSize(MyComponent).
  • Explicit dependencies. The useEffect dependency array makes it clear when the effect re-runs. The HOC hid all of that in the class lifecycle.
  • TypeScript works naturally. The hook is a function with a typed return value. No generic gymnastics needed.

When HOCs Still Make Sense

Hooks are the right default, but HOCs are not completely obsolete. Two cases where I still reach for them:

Providing context value to a subtree. A component factory is sometimes cleaner than importing Context in every leaf. Before hooks, the standard pattern was <Context.Consumer> in every render method. An HOC like withTheme(MyComponent) hid that boilerplate. These days a layout component or a context wrapper is usually a better fit, but the HOC form still shows up in older codebases.

Augmenting display names or metadata. Some testing or storybook setups use HOCs to inject display names for introspection. This is rare and usually temporary.

These are edge cases. If you are writing a new component today and reach for an HOC, ask yourself whether a hook would work. It almost always will.

The Bottom Line

Hooks replaced HOCs (and render props) because they remove the indirection. You do not need to wrap components to share logic. You write a function, call it from anywhere, and get exactly the values you need without naming conflicts or wrapper overhead.

If you maintain an older codebase with HOCs, migrating to hooks is incremental work but pays off in readability and debugging. Every HOC you replace makes one less layer for the next person to unwrap.

← Older
Lifting State Up in React
Newer →
Handling Form Submission in React

Newsletter

A weekly newsletter on React, Next.js, AI-assisted development, and engineering. No spam, unsubscribe any time.