ReactHigher-Order Components (HOCs)

Higher-Order Components (HOCs)

A Higher-Order Component is a function that takes a component and returns a new, enhanced component. It is a pattern for reusing component logic — not a React API, just a convention that emerged from JavaScript's compositional nature.

JSX
// A HOC is just a function: Component in → enhanced Component out
const EnhancedComponent = withSomething(OriginalComponent)

The name typically starts with withwithAuth, withLogger, withTheme, withData. This makes it immediately clear in a codebase that a component has been wrapped.

A Minimal HOC

The simplest possible HOC does nothing but wrap and pass props through. It is a useful starting template:

JSX
function withNothing(WrappedComponent) {
  // The returned component can be a function component or class
  function WithNothing(props) {
    // Pass every prop through — this is critical
    return <WrappedComponent {...props} />
  }

  // Set a display name so React DevTools shows "withNothing(Button)"
  // instead of an anonymous "WithNothing"
  WithNothing.displayName = `withNothing(${
    WrappedComponent.displayName || WrappedComponent.name || 'Component'
  })`

  return WithNothing
}

// Usage
const EnhancedButton = withNothing(Button)
Note
Always pass `{...props}` to the wrapped component. Forgetting to spread props is the most common HOC bug — the wrapped component receives nothing and silently renders empty.
Real Example: withAuth

A classic use case is protecting routes or components behind authentication. The withAuth HOC reads the auth state and either renders the wrapped component or redirects to login:

JSX
import { useRouter } from 'next/navigation'
import { useAuth } from '@/hooks/useAuth'

function withAuth(WrappedComponent) {
  function WithAuth(props) {
    const { user, loading } = useAuth()
    const router = useRouter()

    if (loading) {
      return <div>Loading...</div>
    }

    if (!user) {
      // Redirect to login; in real code call router.replace('/login')
      router.replace('/login')
      return null
    }

    // User is authenticated — render the real component with all its props
    return <WrappedComponent {...props} user={user} />
  }

  WithAuth.displayName = `withAuth(${
    WrappedComponent.displayName || WrappedComponent.name || 'Component'
  })`

  return WithAuth
}

// Protecting a page component
function DashboardPage({ user }) {
  return <h1>Welcome, {user.name}</h1>
}

export default withAuth(DashboardPage)
Composing Multiple HOCs

HOCs compose. You can stack them to add multiple capabilities. The outermost HOC wraps first and the innermost component receives the combined props:

JSX
const EnhancedProfile = withLogger(withAuth(withTheme(ProfilePage)))

// Reading left-to-right: logger wraps (auth wraps (theme wraps ProfilePage))
// ProfilePage receives: ...originalProps + user (from withAuth) + theme (from withTheme)
// withLogger logs every render of whatever is below it

// A compose utility (also available in lodash/fp and Redux) makes this cleaner:
const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x)

const enhance = compose(withLogger, withAuth, withTheme)
const EnhancedProfile = enhance(ProfilePage)
Hoisting Static Methods

One subtle problem: when you wrap a component in a HOC, the static methods of the original component are lost on the returned wrapper. Use the hoist-non-react-statics package to copy them automatically.

JSX
import hoistNonReactStatics from 'hoist-non-react-statics'

function withLogger(WrappedComponent) {
  function WithLogger(props) {
    console.log(`Rendered: ${WrappedComponent.displayName}`)
    return <WrappedComponent {...props} />
  }

  // Copy all static methods from WrappedComponent onto WithLogger
  hoistNonReactStatics(WithLogger, WrappedComponent)

  WithLogger.displayName = `withLogger(${
    WrappedComponent.displayName || WrappedComponent.name
  })`

  return WithLogger
}

// If WrappedComponent had a static getLayout() method, it is now on WithLogger too
HOC Use Cases
  • Authentication / authorization — gate access before rendering the component

  • Logging and analytics — automatically track mounts, unmounts, or specific interactions

  • Data fetching — inject loaded data as a prop (common pre-hooks pattern)

  • Theming — inject theme or style props from a global context

  • Error boundaries — wrapping any component in an error-catching HOC without touching its source

  • Feature flags — conditionally render based on remote config

withLogger: A Practical Debug HOC

JSX
function withLogger(WrappedComponent) {
  const name = WrappedComponent.displayName || WrappedComponent.name

  function WithLogger(props) {
    // Log every time props change
    React.useEffect(() => {
      console.group(`[${name}] rendered`)
      console.log('props:', props)
      console.groupEnd()
    })

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

  WithLogger.displayName = `withLogger(${name})`
  return WithLogger
}

// Wrap any component during development to understand re-render patterns
const DebugUserCard = withLogger(UserCard)
// In production, just swap back to UserCard
HOCs vs Hooks: When to Use Each

Consideration

HOC

Custom Hook

Works in class components

Yes

No

Adds wrapper DOM node

Can (usually div-less if careful)

No

Visible in DevTools

Yes (displayName)

Yes (useName)

Prop name conflicts

Possible (namespace carefully)

No

Composability

Stack via compose()

Just call multiple hooks

Code co-location

Separate file

Inline in component

Warning
Never define a HOC inside another component's render function. Doing so creates a new component type on every render, which causes React to unmount and remount the wrapped component on every render instead of updating it. Always define HOCs at the module level.
When HOCs Are Still the Right Tool

With hooks covering most logic-sharing needs, HOCs are less common in modern codebases. But they remain the best tool when:

  • You are enhancing class components that cannot use hooks

  • Error boundaries — React's error boundary API is class-only; a withErrorBoundary HOC is the standard wrapper

  • Third-party library integration — many HOC-based APIs (Redux's older connect(), old React Router's withRouter) still exist in existing codebases

  • Cross-cutting static enhancements — adding getServerSideProps or getLayout to Next.js pages uniformly

Tip
React DevTools shows the HOC chain in the component tree when you set displayName correctly. This is valuable during debugging, so never skip the displayName assignment when writing a HOC for production use.