Higher-Order Components

A function that takes a component and returns an enhanced one — legacy, but worth understanding.

A higher-order component (HOC) is a function that accepts a component and returns a new component with extra behavior wrapped around it: const Enhanced = withThing(Base). It was the dominant reuse pattern in class-component React, and you will still meet it in older code and some libraries (routers, analytics, connect from React-Redux).

The pattern

function withLogging(Wrapped) {
  return function WithLogging(props) {
    useEffect(() => { console.log('mounted', Wrapped.name); }, []);
    return <Wrapped {...props} />;
  };
}

The wrapper renders the original component, forwarding props and adding whatever it needs — subscriptions, injected props, guards.

The gotchas that made hooks win

  • Wrapper hell — stacking withA(withB(withC(X))) buries components deep in the tree and muddies the React DevTools view.
  • Prop collisions — two HOCs injecting a prop of the same name silently clobber each other.
  • Refs and statics — you must forward refs deliberately and hoist static methods, or they vanish.

Prefer hooks for new code

For sharing logic, a custom hook does the same job with no wrapper, no collisions, and clearer data flow. Reach for an HOC only when you must wrap an arbitrary component you do not control — for example, adding an error boundary or a permission gate around any child.

Example

Example · javascript
import { useState, useEffect } from 'react';

// HOC that gates a component behind a permission check.
function withPermission(Wrapped, required) {
  function Guarded(props) {
    const [status, setStatus] = useState('checking');

    useEffect(() => {
      let active = true;
      hasPermission(required).then((ok) => {
        if (active) setStatus(ok ? 'granted' : 'denied');
      });
      return () => { active = false; };
    }, []);

    if (status === 'checking') return <p>Checking access...</p>;
    if (status === 'denied') return <p>You do not have access.</p>;
    return <Wrapped {...props} />;
  }
  // Preserve a readable name for DevTools.
  Guarded.displayName = `withPermission(${Wrapped.displayName || Wrapped.name})`;
  return Guarded;
}

function hasPermission() {
  return Promise.resolve(true); // stand-in for a real check
}

function AdminPanel() {
  return <div>Secret admin tools</div>;
}

const GuardedAdminPanel = withPermission(AdminPanel, 'admin');

When to use it

  • A withAuth HOC that checks authentication state and redirects unauthenticated users before rendering any protected page component in a legacy React Router 4 codebase.
  • A withLogger HOC wrapping third-party components to track mount/unmount events in an analytics library without modifying components you do not own.
  • A withTheme HOC from an older design-system package injecting a theme object as props into consuming components written before React context existed.

More examples

Basic withAuth HOC

withAuth wraps any page component, checks authentication, and either redirects or passes the user object down as a prop while preserving displayName for DevTools.

Example · js
function withAuth(WrappedComponent) {
  function AuthGuard(props) {
    const user = useCurrentUser();
    if (!user) return <Redirect to="/login" />;
    return <WrappedComponent {...props} user={user} />;
  }
  AuthGuard.displayName =
    `withAuth(${WrappedComponent.displayName || WrappedComponent.name})`;
  return AuthGuard;
}

export default withAuth(DashboardPage);

HOC forwarding refs correctly

Using React.forwardRef inside the HOC ensures that consumers who pass a ref to the enhanced component still reach the underlying element.

Example · js
function withBorder(WrappedComponent) {
  const WithBorder = React.forwardRef((props, ref) => (
    <div style={{ border: '2px solid var(--accent)' }}>
      <WrappedComponent ref={ref} {...props} />
    </div>
  ));
  WithBorder.displayName = `withBorder(${WrappedComponent.name})`;
  return WithBorder;
}

const BorderedInput = withBorder(
  React.forwardRef((props, ref) => <input ref={ref} {...props} />)
);

Migrate HOC logic to a custom hook

The same behavior the HOC provided is cleaner as a custom hook — no extra wrapper node in the tree and no risk of prop-name collisions.

Example · js
// Legacy HOC approach
const EnhancedWidget = withWindowSize(Widget);

// Modern equivalent — prefer hooks for new code
function useWindowSize() {
  const [size, setSize] = React.useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });
  React.useEffect(() => {
    const handler = () =>
      setSize({ width: window.innerWidth, height: window.innerHeight });
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);
  return size;
}

function Widget() {
  const { width, height } = useWindowSize();
  return <div>Viewport: {width}x{height}</div>;
}

Discussion

  • Be the first to comment on this lesson.