What are higher-order components (HOCs), and how do they compare to hooks and render props?
An HOC is a function that takes a component and returns a new component, wrapping it with extra props or behavior. Hooks have largely replaced HOCs for state/logic reuse, but HOCs still shine for cross-cutting wrappers like auth gates, error boundaries, and analytics.
A higher-order component is (Component) => EnhancedComponent — a function that takes a React component and returns a new one with extra capabilities. The pattern predates hooks and was the primary tool for sharing stateful logic in class-component-era React (connect from react-redux, withRouter, withStyles).
The shape.
function withLogger<P>(Wrapped: React.ComponentType<P>) {
return function WithLogger(props: P) {
useEffect(() => { console.log("mounted", Wrapped.displayName); }, []);
return <Wrapped {...props} />;
};
}
const TrackedButton = withLogger(Button);HOC vs hook. Hooks won the logic-reuse war because they avoid wrapper hell, don't collide on prop names, and make data flow explicit. Anything that is "give a component this state/behavior" is now a hook: useAuth(), useTheme(), useFetch(). HOCs that just inject props — replace with hooks.
Where HOCs still win.
- Cross-cutting wrappers that need to render something around the child:
withErrorBoundary(Component),withSuspense(Component, fallback),withAuthGate(Component)(renders<Login />or the wrapped component). - Conditional rendering based on external state without leaking the check into every consumer.
- Library APIs that need a component-level wrapper (e.g.,
React.memo,React.forwardRefare themselves HOC-shaped). - Analytics / instrumentation wrappers applied uniformly to many components.
Render props as the third option. A render prop passes a function-as-children that receives the shared state: <DataFetcher>{({data}) => <View data={data}/>}</DataFetcher>. Same logic-reuse goal as HOCs but inverted control. Today, hooks subsume both for state, while render props persist for things like virtualization libraries that need to give the consumer a render slot (react-window).
HOC pitfalls (and how to avoid them).
- Wrapper hell —
withA(withB(withC(Comp)))is hard to debug and changes the React tree depth. Compose withcompose(functional) or just stop nesting and prefer hooks. - Prop collisions — if two HOCs both inject
user, the inner wins silently. Namespace injected props or document them. - Static methods are lost —
Wrapped.someStaticdoesn't exist onEnhancedunless you copy viahoist-non-react-statics. - Refs don't forward — must use
forwardRefinside the HOC. - Display name — set
Enhanced.displayName = \withX(${Wrapped.displayName ?? Wrapped.name})\`` so React DevTools is readable. - TypeScript — generic HOCs are notoriously fiddly; spreading props correctly while injecting new ones requires careful generics.
Modern React posture. Default to hooks. Reach for an HOC when the abstraction is "wrap this component in a wrapper that decides what to render" — auth gates, feature-flag gates, error boundaries. Use a render-prop / slot pattern when consumers need to inject the rendering logic.
Code
Follow-up questions
- •When would you choose a render prop over an HOC?
- •Why did hooks largely replace HOCs?
- •How do you forward refs through an HOC?
- •What's wrapper hell and how do you avoid it?
Common mistakes
- •Using an HOC for logic that a hook would handle more cleanly.
- •Forgetting hoist-non-react-statics → static methods/properties disappear.
- •Not setting displayName → DevTools shows 'Anonymous'.
- •Two HOCs colliding on the same injected prop name silently.
Performance considerations
- •Each HOC adds a layer to the React tree — usually negligible, but deep stacks bloat profiler output.
- •Wrapping with React.memo at the right level matters more than HOC count.
Edge cases
- •An HOC defined inline inside render creates a new component each render — children remount every time.
- •Class components: copy statics; function components: nothing to copy but check for attached fields.
Real-world examples
- •react-redux's connect, withRouter (legacy), withStyles, Sentry's withErrorBoundary, and React.memo / React.forwardRef themselves.