Master Higher Order Components — how they work, the conventions that prevent hard-to-debug bugs, why hooks largely replaced them, and the cases where HOCs are still the right choice.
A Higher Order Component is a function that takes a component and returns a new, enhanced component. Think of it as a component factory with an extra layer of behaviour baked in — authentication checking, loading states, analytics tracking, or feature flags — without the original component knowing anything about it. The wrapped component just receives props and renders; the HOC handles the cross-cutting concern around it.
A HOC is a function: it accepts a component as an argument and returns a new component that wraps it with additional logic. The naming convention is to prefix with with:
// HOC that redirects unauthenticated users
function withAuth(WrappedComponent) {
return function AuthGuard(props) {
const { user, loading } = useAuth();
if (loading) return <Spinner />;
if (!user) return <Navigate to="/login" />;
return <WrappedComponent {...props} />;
};
}
// Usage
const ProtectedDashboard = withAuth(Dashboard);
// ProtectedDashboard behaves exactly like Dashboard, but checks auth first
<ProtectedDashboard userId={123} />
// ❌ Breaks the wrapped component — props are swallowed
return <WrappedComponent />;
// ✅ Always spread props through so the wrapped component gets everything
return <WrappedComponent {...props} />;
function withAuth(WrappedComponent) {
function AuthGuard(props) { ... }
// React DevTools will show "withAuth(Dashboard)" instead of "AuthGuard"
AuthGuard.displayName = `withAuth(${WrappedComponent.displayName ?? WrappedComponent.name})`;
return AuthGuard;
}
HOCs break ref forwarding by default — a ref on a HOC-wrapped component points to the HOC wrapper, not to the underlying component's DOM node. Fix this with React.forwardRef:
function withLogging(WrappedComponent) {
const WithLogging = React.forwardRef((props, ref) => {
logRender(WrappedComponent.name);
return <WrappedComponent {...props} ref={ref} />;
});
WithLogging.displayName = `withLogging(${WrappedComponent.name})`;
return WithLogging;
}
HOCs compose by nesting function calls — but deep nesting creates "wrapper hell" and makes debugging painful:
// Hard to read and debug — which HOC caused the bug?
const EnhancedComponent = withRouter(withAuth(withAnalytics(withTheme(Dashboard))));
// Use a compose utility (from Redux or Ramda) to flatten the nesting
const enhance = compose(withRouter, withAuth, withAnalytics, withTheme);
const EnhancedComponent = enhance(Dashboard);
Even with compose, HOC-wrapped components are hard to see in React DevTools because each HOC adds a layer to the component tree.
Custom hooks solve most of what HOCs were used for — without the component nesting, prop collision, or ref forwarding complexity:
// HOC approach — adds a component layer, injects props
const ProtectedDashboard = withAuth(Dashboard);
// Custom hook approach — logic lives inside the component, no wrapping
function Dashboard() {
const { user, loading } = useAuth(); // same logic, no wrapper
if (loading) return <Spinner />;
if (!user) return <Navigate to="/login" />;
return <DashboardContent />;
}
Hooks compose naturally, don't add tree depth, can't collide on prop names, and are visible in the component's own code. For new code, prefer custom hooks over HOCs.
connect() from Redux v4, withRouter, etc., HOCs are already the established pattern.Many developers think HOCs are deprecated or wrong — they're a valid pattern, especially when working with class components or HOC-based libraries like Redux v4's connect(). React.memo is itself a HOC. They're less necessary in modern functional-component codebases, but not deprecated.
Many developers forget to spread props in a HOC and wonder why the wrapped component isn't receiving its props — the most basic HOC mistake is returning <WrappedComponent /> without {...props}. The HOC swallows everything and the wrapped component gets nothing.
Many developers don't set displayName and then can't debug their apps — React DevTools shows 'Component' or a random function name instead of 'withAuth(Dashboard)'. Always set displayName explicitly; it takes one line and saves hours of debugging.
Many developers don't realise HOCs break refs by default — a ref attached to a HOC-wrapped component points to the HOC's function component, not the underlying DOM node or class instance. Every HOC that might have a ref applied to it must use React.forwardRef.
Many developers use HOCs when a custom hook would be simpler — if the logic doesn't need to render anything (auth check, data fetching, event listeners), a custom hook expresses it more clearly with no component nesting, no prop injection, and no ref forwarding complexity.
Many developers define HOCs inside another component's render — defining a HOC inside render creates a new component type on every render, which makes React unmount and remount the wrapped component on every parent re-render. Always define HOCs at the module level, outside of any component.
Redux connect(): the classic HOC — connect(mapStateToProps, mapDispatchToProps)(MyComponent) wraps a component to inject Redux store state and dispatch as props. Still present in millions of React codebases.
Route protection: withAuth(Dashboard) is a ubiquitous pattern in older Next.js and React Router codebases — check authentication, redirect if not logged in, render the page if authenticated.
React.memo: React.memo(ExpensiveComponent) is a built-in HOC — it wraps the component and skips re-renders when props haven't changed. The fact that a standard React API is a HOC shows the pattern is still valid.
Error tracking HOCs: some error monitoring SDKs provide a withErrorTracking(Component) HOC that wraps any component to automatically report errors thrown during its render lifecycle.
A/B testing: a withVariant(Component, 'experiment-name') HOC injects the current experiment variant as a prop, letting components render variant-specific UI without managing experiment state themselves.
Storybook decorators: Storybook's story decorators are essentially HOCs applied to stories — they wrap each story in a provider (theme, router, Redux store) so each story has the context it needs to render.
No questions tagged to this topic yet.
Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.