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.
// A HOC is just a function: Component in → enhanced Component out const EnhancedComponent = withSomething(OriginalComponent)
The name typically starts with with — withAuth, 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:
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)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:
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:
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.
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 tooHOC 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
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 UserCardHOCs 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 |
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
withErrorBoundaryHOC is the standard wrapperThird-party library integration — many HOC-based APIs (Redux's older
connect(), old React Router'swithRouter) still exist in existing codebasesCross-cutting static enhancements — adding
getServerSidePropsorgetLayoutto Next.js pages uniformly