Topic #25Core14 min read

HOC (Higher Order Components)

Logic-reuse patterns in React compared head to head: Higher Order Components (what they are, how to write them correctly, the pitfalls), render props, and custom hooks — why hooks are the modern default, and where HOCs still legitimately appear.

#react#hoc#patterns#hooks#render-props#composition#custom-hooks#wrapper-hell

The underlying problem: reusing behaviour, not markup. React makes reusing UI easy — you extract a component. The harder problem is reusing behaviour: an auth check that redirects, a loading spinner while data arrives, subscribing to a data source, logging, injecting props. Over React's history three patterns emerged to share this cross-cutting logic without copy-pasting it into every component: Higher Order Components (HOCs), render props, and — since React 16.8 — custom hooks. Understanding all three, and why hooks won, is a classic interview theme.

What a Higher Order Component is. An HOC is a function that takes a component and returns a new component with extra behaviour wrapped around it. The name mirrors 'higher-order function' (a function that takes/returns a function). The canonical shape is const Enhanced = withSomething(Component). The HOC owns the shared logic once (auth, loading, data injection) and produces an augmented component; you never edit the wrapped component itself. Historic examples include Redux's connect(mapState, mapDispatch)(Component) and React Router's withRouter.

How an HOC works internally. Inside, the HOC defines a new function component that runs the shared logic, then renders the wrapped component — usually forwarding the incoming props with the spread operator (<Wrapped {...props} />) and often injecting extra props. So withAuth(Dashboard) returns a component that reads the current user, redirects if there is none, and otherwise renders <Dashboard {...props} />. The wrapped component stays oblivious to the auth logic; it just receives props.

Writing HOCs correctly — the pitfalls that break them. HOCs have well-known footguns. (1) Always spread incoming props through to the wrapped component, or you silently swallow props it needs. (2) Do not create the HOC inside renderrender() { return withAuth(Dashboard) } produces a brand-new component type every render, which unmounts and remounts the whole subtree, destroying its state; create it once at module scope. (3) Copy static methods — the returned component does not automatically carry the wrapped component's statics (use hoist-non-react-statics). (4) Set a meaningful displayName (e.g. withAuth(Dashboard)) so React DevTools is readable. (5) Forward refs if callers need to reach the underlying DOM node/component (React.forwardRef).

Render props — the other classic pattern. A render prop is a component that takes a function as a prop (often children) and calls it with the data it manages: <Mouse>{({ x, y }) => <p>{x},{y}</p>}</Mouse>. The Mouse component owns the behaviour (tracking the cursor) and hands the values to whatever the caller wants to render. It is more flexible than an HOC because the caller controls the output inline, but it produces deeply nested 'callback pyramids' when several are combined, and it litters the tree with wrapper components.

Custom hooks — the modern default. A custom hook is just a function whose name starts with use and which calls other hooks. It extracts stateful logic into a reusable function without adding any component to the tree: const { user } = useAuth(); or const { data, loading } = useFetch(url);. This is the key advantage — hooks share logic, whereas HOCs and render props share logic by wrapping components. Since React 16.8, custom hooks are the recommended way to reuse stateful behaviour, and most HOC/render-prop use cases are cleaner as hooks.

Why hooks beat HOCs and render props for new code. Hooks add no wrapper components, so there is no 'wrapper hell' — the nesting of <A><B><C> providers/HOCs that makes the tree and DevTools unreadable. Prop sources are explicit: with an HOC you must trace which of several HOCs injected a given prop, whereas a hook's return value is right there at the call site. Hooks compose trivially — call several in one component — and they are far easier to type in TypeScript and to test in isolation. Render props avoid wrapper types but reintroduce nesting through callbacks; hooks avoid both problems.

Where HOCs and render props still legitimately appear. HOCs are not dead. They remain the natural fit when you must wrap a component you do not control or need to alter what it renders (not just supply data) — cross-cutting concerns like error boundaries, feature flags, analytics wrappers, or third-party libraries whose public API is an HOC. Some libraries still ship connect-style HOCs. Render props survive in component libraries where the caller must control rendering (e.g. virtualised lists, headless UI components). But the default for your own reusable logic is a custom hook.

A note on composition philosophy. All three patterns are expressions of React's core idea: composition over inheritance. React deliberately avoids class-inheritance-based reuse (mixins were removed for good reasons — name clashes, unclear dependencies). HOCs compose by wrapping, render props compose by delegating rendering, and hooks compose by calling. Hooks are the most direct form of composition because they operate on logic itself rather than on the component boundary.

The mental model (memorise this). All three patterns exist to reuse cross-cutting behaviour, not markup. An HOC is a function that takes a component and returns an enhanced one (wrapping); a render prop passes a render function so the caller controls output (delegation); a custom hook is a use-prefixed function that shares logic with no wrapper at all (composition of logic). Prefer custom hooks for new code — no wrapper hell, explicit prop sources, trivial typing and testing — and reserve HOCs for wrapping components you do not own or altering what they render.

Backend Analogy

HOCs are the frontend's decorator/proxy pattern: `withAuth(Dashboard)` wraps a target with a proxy that runs a cross-cutting concern (an auth check) before delegating to the wrapped implementation — exactly like a Spring AOP `@Around` advice or a servlet filter that intercepts a request, does its work, then passes control down the chain. Render props are the strategy/template-method pattern — the component runs the invariant algorithm and calls back into a caller-supplied strategy for the variable rendering step. Custom hooks are composition via dependency-free helper functions or injected collaborators: instead of subclassing or wrapping, a class simply calls the shared service it needs. The move from HOCs to hooks mirrors the move from inheritance/AOP-wrapping toward plain composition — the same reason 'favour composition over inheritance' is a bedrock Java design principle: wrappers stack into unreadable proxy chains (wrapper hell), while calling a collaborator keeps dependencies explicit at the call site.

Key Insights
  • HOCs, render props, and custom hooks all exist to reuse cross-cutting behaviour (auth, loading, data), not UI markup.
  • An HOC is a function that takes a component and returns an enhanced component; it wraps logic around the original without editing it.
  • HOCs work by rendering the wrapped component with props spread through and often extra props injected.
  • HOC pitfalls: always spread props, never create the HOC inside render (it remounts and loses state), copy static methods, set displayName, and forward refs.
  • A render prop passes a function (often children) that the component calls with its managed data, letting the caller control rendering.
  • A custom hook is a use-prefixed function that shares logic WITHOUT adding a component to the tree — the key difference from HOCs and render props.
  • Prefer custom hooks for new code: no wrapper hell, explicit prop sources, trivial composition, easier typing and testing.
  • Render props avoid wrapper types but reintroduce nesting via callback pyramids; hooks avoid both.
  • HOCs still fit when wrapping a component you do not control or altering what it renders (error boundaries, analytics, feature flags, some libraries).
  • All three express composition over inheritance; hooks compose logic most directly, which is why mixins were abandoned.

Worked Code

HOC done right — withAuth with spread props, displayName, and forwardRef
TSX
import React from 'react';

// An HOC: takes a component, returns an enhanced component.
function withAuth<P extends object>(Wrapped: React.ComponentType<P>) {
  // Define the new component ONCE at module scope — never inside render.
  const WithAuth = React.forwardRef<unknown, P>((props, ref) => {
    const { user } = useAuth();
    if (!user) return <Navigate to="/login" />;
    // Always spread incoming props through to the wrapped component.
    return <Wrapped {...(props as P)} ref={ref} />;
  });

  // Readable name in React DevTools: "withAuth(Dashboard)".
  WithAuth.displayName = `withAuth(${Wrapped.displayName || Wrapped.name || 'Component'})`;
  return WithAuth;
}

// Usage: create the enhanced component once, then render it anywhere.
const ProtectedDashboard = withAuth(Dashboard);
// <ProtectedDashboard /> redirects to /login if there is no user.

declare function useAuth(): { user: unknown };
declare function Navigate(p: { to: string }): JSX.Element;
declare function Dashboard(p: object): JSX.Element;
The same behaviour as a render prop
TSX
import { useState, useEffect, ReactNode } from 'react';

// Render prop: the component owns the behaviour and calls children(data).
function MousePosition({ children }: { children: (pos: { x: number; y: number }) => ReactNode }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  useEffect(() => {
    const onMove = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', onMove);
    return () => window.removeEventListener('mousemove', onMove);
  }, []);
  return <>{children(pos)}</>; // caller controls the rendering
}

// Usage — flexible, but note the nesting/callback pyramid when combined.
function App() {
  return (
    <MousePosition>
      {({ x, y }) => <p>Mouse at {x}, {y}</p>}
    </MousePosition>
  );
}
The modern default — the same logic as a custom hook (no wrapper)
TSX
import { useState, useEffect } from 'react';

// A custom hook: a use-prefixed function that shares LOGIC, adding no
// component to the tree. Prop sources are explicit at the call site.
function useMousePosition() {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  useEffect(() => {
    const onMove = (e: MouseEvent) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', onMove);
    return () => window.removeEventListener('mousemove', onMove);
  }, []);
  return pos;
}

// Usage: no wrapper hell, trivially composable with other hooks.
function App() {
  const { x, y } = useMousePosition();
  return <p>Mouse at {x}, {y}</p>;
}
A withLoading HOC — a case that still fits HOCs (altering what renders)
TSX
import React from 'react';

// HOCs shine when you alter WHAT is rendered, not just inject data —
// here: short-circuit to a spinner while loading.
function withLoading<P extends object>(
  Wrapped: React.ComponentType<P>,
) {
  function WithLoading({ loading, ...rest }: P & { loading: boolean }) {
    if (loading) return <Spinner />;
    return <Wrapped {...(rest as P)} />;
  }
  WithLoading.displayName = `withLoading(${Wrapped.displayName || Wrapped.name || 'Component'})`;
  return WithLoading;
}

const UserListWithLoading = withLoading(UserList);
// <UserListWithLoading loading={isLoading} users={users} />

declare function Spinner(): JSX.Element;
declare function UserList(p: { users: unknown[] }): JSX.Element;

Interview-Ready Q&A

An HOC is a function that takes a component and returns a new component with extra behaviour wrapped around it — for example withAuth(Dashboard) returns a component that checks the user and redirects if unauthenticated before rendering Dashboard. It mirrors the idea of a higher-order function and lets you reuse cross-cutting logic without editing the wrapped component itself. Classic examples are Redux's connect and React Router's withRouter.

Things to Remember
  • 1HOCs, render props, and hooks all reuse cross-cutting behaviour, not markup.
  • 2HOC = function that takes a component and returns an enhanced component (wrapping).
  • 3HOCs render the wrapped component with props spread through, often injecting extra props.
  • 4HOC footguns: spread props, never build the HOC in render, copy statics, set displayName, forward refs.
  • 5Render prop = pass a function (often children) the component calls with its data; caller controls rendering.
  • 6Custom hook = use-prefixed function sharing logic with NO wrapper component — the decisive advantage.
  • 7Prefer hooks for new code: no wrapper hell, explicit prop sources, easy composition, typing, and testing.
  • 8Render props avoid wrapper types but cause callback-pyramid nesting; hooks avoid both.
  • 9Keep HOCs for wrapping components you do not own or altering what they render (error boundaries, analytics).
  • 10All three favour composition over inheritance; hooks compose logic most directly (why mixins were dropped).

References & Further Reading