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 hellwithA(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 lostWrapped.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.