React

Higher-Order Components

10 Questions

A Higher-Order Component (HOC) is a function that accepts a component and returns a new, enhanced component with additional props, logic, or behavior injected. HOCs are a pattern for reusing component logic across multiple components without modifying any of them directly. Common use cases include adding authentication checks, loading states, logging, feature flags, and data subscriptions. The returned component typically renders the original component and passes all props through to it.
function withLogger(WrappedComponent) {
  return function EnhancedComponent(props) {
    useEffect(() => {
      console.log('Component rendered:', WrappedComponent.name);
    });
    return <WrappedComponent {...props} />;
  };
}

const LoggedButton = withLogger(Button);
<LoggedButton label="Click" />
HOCs follow the pattern const Enhanced = higherOrderComponent(Original). They are a composition pattern, not part of the React API.

Why it matters: HOCs let you add shared behavior (like auth checks or analytics) to multiple components without repeating code in each one.

Real applications: Authentication wrappers, loading state wrappers, analytics event tracking, permission checks.

Common mistakes: Mutating the wrapped component inside the HOC instead of extending it — this breaks reusability and causes unexpected side effects.

To create an HOC, define a function that takes a component as its argument, creates a new wrapper component that adds the desired logic, and passes all original props through to the wrapped component using spread syntax. The HOC should only add behavior — it should not swallow or modify props the wrapped component expects. Name the wrapper component after the HOC for better debugging visibility in React DevTools.
function withLoading(WrappedComponent) {
  return function WithLoadingComponent({ isLoading, ...rest }) {
    if (isLoading) return <div>Loading...</div>;
    return <WrappedComponent {...rest} />;
  };
}

const UserListWithLoading = withLoading(UserList);

// Usage
<UserListWithLoading isLoading={loading} users={users} />
The HOC intercepts the isLoading prop and either shows a spinner or delegates to the wrapped component with the remaining props.

Why it matters: A loading HOC adds the same loading state behavior to any component without duplicating the spinner logic.

Real applications: Adding a loading spinner to any data-fetching component, adding a skeleton screen to any card component.

Common mistakes: Forgetting to pass through all props to the wrapped component — some props will silently disappear and the wrapped component won't receive them.

An authentication HOC checks whether the user is logged in before rendering the wrapped component. If authenticated, it renders the component normally; if not, it redirects to the login page using React Router's <Navigate>. This pattern centralizes the auth check in one place so every protected route gets the same consistent behavior without repeating the check in each component. Pass the current auth state via context rather than props to keep wrapped components decoupled from auth logic.
function withAuth(WrappedComponent) {
  return function AuthenticatedComponent(props) {
    const { isAuthenticated } = useAuth();

    if (!isAuthenticated) {
      return <Navigate to="/login" />;
    }

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

const ProtectedDashboard = withAuth(Dashboard);
const ProtectedSettings = withAuth(Settings);

// Usage
<Route path="/dashboard" element={<ProtectedDashboard />} />
This centralizes auth logic so each protected component doesn't need to check authentication individually.

Why it matters: An auth HOC keeps authentication checks in one place — any component wrapped with it is automatically protected.

Real applications: Protecting dashboard pages, admin panels, user profile pages — anywhere only authenticated users should have access.

Common mistakes: Doing the auth check inside every page component instead of in a single HOC or wrapper — this leads to duplicated logic that's easy to forget on new pages.

HOCs wrap components from the outside and inject props, making the wrapping invisible inside the component. Custom hooks share logic within a component without adding any wrapper to the component tree. Since hooks were introduced in React 16.8, custom hooks are the modern preferred approach — they're simpler, have no nesting overhead, and work naturally with TypeScript. HOCs are still seen in older codebases and in specific patterns like class component wrappers.
// HOC approach — wraps the component
const EnhancedList = withData(List, '/api/items');
// List receives data as a prop injected by the HOC

// Hook approach — logic inside the component
function List() {
  const { data } = useFetch('/api/items');
  return data.map(item => <div key={item.id}>{item.name}</div>);
}
Hooks are more transparent and compose better. HOCs can cause "wrapper hell" and make it harder to trace where props come from. Use hooks for new code.

Why it matters: Custom hooks are simpler to use, easier to test, and don't add wrapper components to the component tree.

Real applications: Modern React codebases use custom hooks for reusable logic; HOCs are mainly found in older codebases or libraries.

Common mistakes: Writing a new HOC when a custom hook would do the same job with less code and no extra wrapper component in the DevTools tree.

Pass all props from the HOC wrapper down to the wrapped component using the spread operator: {...props}. If the HOC injects its own props (like theme or isLoading), destructure those out first and spread the rest to avoid passing HOC-specific internal props to the DOM element. This prevents React warnings about unknown DOM attributes and ensures the wrapped component receives exactly what it expects.
function withTheme(WrappedComponent) {
  return function ThemedComponent(props) {
    const theme = useContext(ThemeContext);
    // Pass all original props plus the new theme prop
    return <WrappedComponent {...props} theme={theme} />;
  };
}

// All props passed to ThemedButton reach Button
const ThemedButton = withTheme(Button);
<ThemedButton size="lg" onClick={handleClick} />
// Button receives: { size, onClick, theme }
Always spread props to avoid accidentally swallowing props meant for the wrapped component.

Why it matters: If you don't spread all props through, the wrapped component silently loses props and can behave unexpectedly without any error.

Real applications: Any HOC that wraps a component — always use {...props} to forward everything the parent passes.

Common mistakes: Picking out specific props without also forwarding the rest with the spread operator, breaking the wrapped component.

Without a displayName, React DevTools shows wrapped components as Component or Anonymous, making debugging nearly impossible in large trees. Set WrappedComponent.displayName on the HOC's wrapper to show a readable name like withData(Button). This is a small convention that makes a huge difference when tracing component trees in DevTools. Most professional HOC implementations include this pattern automatically.
function withData(WrappedComponent) {
  function WithDataComponent(props) {
    // ... data fetching logic
    return <WrappedComponent {...props} />;
  }

  WithDataComponent.displayName =
    `WithData(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;

  return WithDataComponent;
}

// In DevTools: WithData(UserList) instead of Anonymous
This convention makes debugging much easier, especially when multiple HOCs are composed together.

Why it matters: Without a displayName, all HOC-wrapped components show as "Component" or "Unknown" in React DevTools, making debugging very hard.

Real applications: Any HOC used in production — always set displayName so you can identify components in the DevTools component tree.

Common mistakes: Forgetting to set displayName and then spending time debugging which component is which in a deeply-nested HOC composition.

The three most common HOC mistakes are: creating the HOC inside a render function (React sees a new component type every render and fully remounts the wrapped component), mutating the original component (modifying its prototype instead of wrapping), and prop naming collisions where the HOC injects a prop with the same name as one the wrapped component already uses. Always create HOCs outside of render, never mutate the original, and document which props your HOC injects.
// ❌ Don't create HOCs inside render — remounts every time
function App() {
  const Enhanced = withLoading(List); // new component each render!
  return <Enhanced />;
}

// ✅ Create HOCs outside the component
const Enhanced = withLoading(List);
function App() {
  return <Enhanced />;
}

// ❌ Don't mutate the original component
// WrappedComponent.prototype.render = ... // never do this
Also, static methods are not automatically copied to the HOC wrapper. Use hoist-non-react-statics if needed.

Why it matters: HOC pitfalls can cause subtle bugs that are hard to debug — knowing them upfront saves a lot of time.

Real applications: Every project using HOCs — always define them outside components and copy over static methods when needed.

Common mistakes: Defining an HOC inside a component function — a new wrapper class is created on every render, causing unexpected remounts and losing state.

By default, refs passed to an HOC point to the HOC's wrapper component, not the inner wrapped component's DOM node. Use React.forwardRef inside the HOC to forward the ref through to the wrapped component. This is essential for HOC-wrapped input components or any element that a parent needs to invoke imperative methods on. Without ref forwarding, all refs to HOC-wrapped components would return the HOC wrapper, not the inner element.
function withTooltip(WrappedComponent) {
  const WithTooltip = React.forwardRef((props, ref) => {
    return (
      <div className="tooltip-wrapper">
        <WrappedComponent {...props} ref={ref} />
      </div>
    );
  });

  WithTooltip.displayName = `WithTooltip(${WrappedComponent.displayName || WrappedComponent.name})`;
  return WithTooltip;
}

const FancyInput = withTooltip(React.forwardRef((props, ref) => (
  <input ref={ref} {...props} />
)));
Without forwardRef, the ref would attach to the HOC wrapper rather than the underlying DOM element.

Why it matters: Without forwardRef in an HOC, a parent's ref will point to the wrapper component, not the actual DOM element the parent needs to control.

Real applications: Wrapping input components with an HOC while still allowing the parent to call .focus() on the actual input element.

Common mistakes: Forgetting to wrap the HOC in forwardRef and wondering why ref.current is the HOC wrapper instead of the DOM node.

You can apply multiple HOCs to a single component by nesting calls or using a compose utility for more readable left-to-right application order. Nesting reads right-to-left (the innermost HOC is applied first), while compose(withA, withB)(Component) reads left-to-right. As the number of HOCs grows, the composition tree in DevTools becomes deeply nested — this is one of the reasons custom hooks replaced HOCs in most modern codebases.
// Nested calls (read inside-out)
const Enhanced = withAuth(withLoading(withTheme(UserList)));

// Using a compose utility (read top-to-bottom)
const compose = (...fns) => (component) =>
  fns.reduceRight((acc, fn) => fn(acc), component);

const Enhanced = compose(
  withAuth,
  withLoading,
  withTheme
)(UserList);
Each HOC wraps the result of the previous one. With many HOCs, the wrapper hierarchy can become deep — prefer hooks for new code to avoid this complexity.

Why it matters: Knowing how to compose HOCs lets you layer multiple behaviors (auth, analytics, loading) onto a single component cleanly.

Real applications: Using a compose utility from Redux or Ramda to apply auth + logging + loading HOCs to a dashboard component.

Common mistakes: Stacking too many HOCs manually — use a compose utility function to keep it readable, or switch to custom hooks instead.

The render props pattern passes a function as a prop (often named render or children) that the component calls to produce its output, injecting shared state or logic as function arguments. It solves the same code-reuse problem as HOCs but avoids the wrapper nesting issue since no extra component is created in the tree. Like HOCs, render props have largely been replaced by custom hooks in modern codebases, but are still found in third-party libraries and older React code.
// Render prop component
function MouseTracker({ children }) {
  const [pos, setPos] = useState({ x: 0, y: 0 });
  useEffect(() => {
    const handle = (e) => setPos({ x: e.clientX, y: e.clientY });
    window.addEventListener('mousemove', handle);
    return () => window.removeEventListener('mousemove', handle);
  }, []);
  return children(pos);
}

// Usage
<MouseTracker>
  {({ x, y }) => <p>Mouse: {x}, {y}</p>}
</MouseTracker>
Both render props and HOCs share stateful logic. Custom hooks have largely replaced both patterns in modern React code.

Why it matters: Understanding how render props relate to HOCs helps you recognize different ways of sharing logic and know which is best for a given situation.

Real applications: Render props are still used in some libraries (like Formik or Downshift); HOCs in older codebases and Redux connect().

Common mistakes: Choosing render props or HOCs for new code when a custom hook would be simpler and easier to test.