Back to React
React
medium
mid

How do Higher Order Components compare to hooks and render props in React?

An HOC is a function that takes a component and returns a new component, wrapping it with extra props or behavior. Hooks have largely replaced HOCs for state/logic reuse, but HOCs still shine for cross-cutting wrappers like auth gates, error boundaries, and analytics.

6 min read·~15 min to think through

A higher-order component is (Component) => EnhancedComponent — a function that takes a React component and returns a new one with extra capabilities. The pattern predates hooks and was the primary tool for sharing stateful logic in class-component-era React (connect from react-redux, withRouter, withStyles).

The shape.

tsx
function withLogger<P>(Wrapped: React.ComponentType<P>) {
  return function WithLogger(props: P) {
    useEffect(() => { console.log("mounted", Wrapped.displayName); }, []);
    return <Wrapped {...props} />;
  };
}
const TrackedButton = withLogger(Button);

HOC vs hook. Hooks won the logic-reuse war because they avoid wrapper hell, don't collide on prop names, and make data flow explicit. Anything that is "give a component this state/behavior" is now a hook: useAuth(), useTheme(), useFetch(). HOCs that just inject props — replace with hooks.

Where HOCs still win.

  • Cross-cutting wrappers that need to render something around the child: withErrorBoundary(Component), withSuspense(Component, fallback), withAuthGate(Component) (renders <Login /> or the wrapped component).
  • Conditional rendering based on external state without leaking the check into every consumer.
  • Library APIs that need a component-level wrapper (e.g., React.memo, React.forwardRef are themselves HOC-shaped).
  • Analytics / instrumentation wrappers applied uniformly to many components.

Render props as the third option. A render prop passes a function-as-children that receives the shared state: <DataFetcher>{({data}) => <View data={data}/>}</DataFetcher>. Same logic-reuse goal as HOCs but inverted control. Today, hooks subsume both for state, while render props persist for things like virtualization libraries that need to give the consumer a render slot (react-window).

HOC pitfalls (and how to avoid them).

  1. Wrapper hell — withA(withB(withC(Comp))) is hard to debug and changes the React tree depth. Compose with compose (functional) or just stop nesting and prefer hooks.
  2. Prop collisions — if two HOCs both inject user, the inner wins silently. Namespace injected props or document them.
  3. Static methods are lost — Wrapped.someStatic doesn't exist on Enhanced unless you copy via hoist-non-react-statics.
  4. Refs don't forward — must use forwardRef inside the HOC.
  5. Display name — set Enhanced.displayName = \withX(${Wrapped.displayName ?? Wrapped.name})\`` so React DevTools is readable.
  6. TypeScript — generic HOCs are notoriously fiddly; spreading props correctly while injecting new ones requires careful generics.

Modern React posture. Default to hooks. Reach for an HOC when the abstraction is "wrap this component in a wrapper that decides what to render" — auth gates, feature-flag gates, error boundaries. Use a render-prop / slot pattern when consumers need to inject the rendering logic.

Code

tsx
function withAuthGate<P extends object>(Wrapped: React.ComponentType<P>) {
  const Gated: React.FC<P> = (props) => {
    const { user, loading } = useAuth();
    if (loading) return <Spinner />;
    if (!user) return <Navigate to="/login" replace />;
    return <Wrapped {...props} />;
  };
  Gated.displayName = `withAuthGate(${Wrapped.displayName ?? Wrapped.name ?? "Component"})`;
  return Gated;
}

const ProtectedDashboard = withAuthGate(Dashboard);
withAuthGate — a justified HOC use
tsx
function withLogger<P, R>(Wrapped: React.ForwardRefExoticComponent<P & React.RefAttributes<R>>) {
  const Logged = React.forwardRef<R, P>((props, ref) => {
    useEffect(() => { console.log("mount"); }, []);
    return <Wrapped {...(props as P)} ref={ref} />;
  });
  Logged.displayName = `withLogger(${Wrapped.displayName ?? "Component"})`;
  return Logged;
}
Forwarding refs through an HOC

Follow-up questions

  • •When would you choose a render prop over an HOC?
  • •Why did hooks largely replace HOCs?
  • •How do you forward refs through an HOC?
  • •What's wrapper hell and how do you avoid it?

Common mistakes

  • •Using an HOC for logic that a hook would handle more cleanly.
  • •Forgetting hoist-non-react-statics → static methods/properties disappear.
  • •Not setting displayName → DevTools shows 'Anonymous'.
  • •Two HOCs colliding on the same injected prop name silently.

Performance considerations

  • •Each HOC adds a layer to the React tree — usually negligible, but deep stacks bloat profiler output.
  • •Wrapping with React.memo at the right level matters more than HOC count.

Edge cases

  • •An HOC defined inline inside render creates a new component each render — children remount every time.
  • •Class components: copy statics; function components: nothing to copy but check for attached fields.

Real-world examples

  • •react-redux's connect, withRouter (legacy), withStyles, Sentry's withErrorBoundary, and React.memo / React.forwardRef themselves.

Senior engineer discussion

Senior signal: knowing when an HOC is the right tool (cross-cutting render-time wrappers) vs when to prefer a hook, and being fluent with refs/static-hoisting/displayName hygiene.