Skip to content
React rc patterns 4 min read

Higher-Order Components

A higher-order component (HOC) is a function that takes a component and returns a new component with extra behavior baked in. It is React’s classic answer to cross-cutting concerns such as authentication, logging, and data fetching — logic you want to share across many unrelated components without copying it everywhere. HOCs predate hooks, and while hooks now cover most of these cases, you will still meet HOCs in libraries (connect, withRouter, React.memo) and legacy code, so understanding them remains essential.

What a higher-order component is

The pattern is a single idea expressed in plain JavaScript: a function whose input is a component and whose output is another component. The returned component renders the original one, usually injecting additional props.

// withLogger.jsx
export function withLogger(WrappedComponent) {
  return function WithLogger(props) {
    console.log(`[render] ${WrappedComponent.name}`, props);
    return <WrappedComponent {...props} />;
  };
}

You apply it by calling the function with a component:

import { withLogger } from "./withLogger";

function Button({ label }) {
  return <button>{label}</button>;
}

export default withLogger(Button);

Output:

[render] Button { label: "Save" }

The HOC does not mutate Button. It creates a brand-new component that wraps it — a pure transformation, which keeps the original reusable elsewhere.

Forwarding props

A well-behaved HOC is transparent: it passes through every prop it receives so the wrapped component behaves as if the HOC were not there. Spreading {...props} is the key. The HOC may also inject its own props on top.

// withUser.jsx
import { useContext } from "react";
import { AuthContext } from "./AuthContext";

export function withUser(WrappedComponent) {
  return function WithUser(props) {
    const { user } = useContext(AuthContext);
    // Pass original props through AND add `user`.
    return <WrappedComponent {...props} user={user} />;
  };
}

Now any wrapped component receives a user prop in addition to whatever its parent passed.

A practical withAuth HOC

The most common real-world HOC gates rendering behind authentication. It reads the current user, redirects unauthenticated visitors, and otherwise renders the protected page untouched.

// withAuth.jsx
import { useContext } from "react";
import { Navigate } from "react-router-dom";
import { AuthContext } from "./AuthContext";

export function withAuth(WrappedComponent) {
  function WithAuth(props) {
    const { user, loading } = useContext(AuthContext);

    if (loading) return <p>Checking session…</p>;
    if (!user) return <Navigate to="/login" replace />;

    return <WrappedComponent {...props} user={user} />;
  }

  WithAuth.displayName = `withAuth(${getDisplayName(WrappedComponent)})`;
  return WithAuth;
}

function getDisplayName(Component) {
  return Component.displayName || Component.name || "Component";
}

Usage stays declarative at the export site:

function Dashboard({ user }) {
  return <h1>Welcome back, {user.name}</h1>;
}

export default withAuth(Dashboard);

Setting displayName

By default the wrapper shows up as WithAuth in React DevTools, which makes deep trees hard to read. Always set a displayName that names the HOC and the component it wraps, as shown above. This produces useful labels like withAuth(Dashboard) instead of a generic wrapper name, and the getDisplayName helper handles components that only expose name.

Tip: Define the inner component as a named function (not an anonymous arrow) and set displayName on it. Anonymous components render as Anonymous in stack traces and warnings, which destroys your debugging signal.

Pitfalls

HOCs solve real problems but carry sharp edges. Stacking several of them creates “wrapper hell” — a tower of nested components that bloats the tree and obscures which props come from where.

PitfallWhy it hurtsMitigation
Wrapper hellwithA(withB(withC(X))) deepens the tree and the DevTools viewPrefer hooks, or compose with a small compose helper
Prop collisionsTwo HOCs inject the same prop name; one silently winsNamespace injected props or document them clearly
Lost staticsdefaultProps, propTypes are not copied to the wrapperUse hoist-non-react-statics
Broken refsA ref points at the wrapper, not the inner componentForward it with React.forwardRef
New component each renderCalling the HOC inside render remounts the subtreeApply the HOC once at module scope

The last row is the most damaging: never call withAuth(SomeComponent) inside another component’s body. Each render produces a different component identity, so React unmounts and remounts the entire subtree, throwing away its state.

The hooks alternative

Most logic that once lived in HOCs now belongs in a custom hook. Hooks share behavior without adding to the component tree, sidestep prop collisions, and read top-to-bottom. The withAuth example collapses into a hook plus an explicit guard.

// useAuth.js
import { useContext } from "react";
import { AuthContext } from "./AuthContext";

export function useAuth() {
  return useContext(AuthContext);
}
// Dashboard.jsx
import { Navigate } from "react-router-dom";
import { useAuth } from "./useAuth";

export default function Dashboard() {
  const { user, loading } = useAuth();

  if (loading) return <p>Checking session…</p>;
  if (!user) return <Navigate to="/login" replace />;

  return <h1>Welcome back, {user.name}</h1>;
}

Reach for an HOC only when you genuinely need to wrap the rendering of a component you do not control, or to match a library’s existing HOC API. For sharing stateful logic in your own code, a custom hook is almost always cleaner.

Best Practices

  • Always spread incoming props ({...props}) so the HOC stays transparent.
  • Set a descriptive displayName like withAuth(Dashboard) for readable DevTools and stack traces.
  • Apply HOCs once at module scope, never inside another component’s render.
  • Copy non-React statics with hoist-non-react-statics and forward refs with React.forwardRef.
  • Document which props an HOC injects to avoid silent name collisions.
  • Prefer a custom hook for sharing logic in your own code; reserve HOCs for wrapping rendering or matching library APIs.
Last updated June 14, 2026
Was this helpful?