design-pattern
Higher-Order Component (HOC) React Design Pattern
I needed to add analytics tracking to 12 different components. Each one had to log when it mounted and when the user interacted with it. Copy-pasting the ...
14 Apr 2024

I needed to add analytics tracking to 12 different components. Each one had to log when it mounted and when the user interacted with it. Copy-pasting the same useEffect into every component was not an option.
A Higher-Order Component (HOC) is a function that takes a component and returns a new component with added behavior. The original component doesn't change. The HOC wraps it, injects extra functionality, and passes everything else through.
Think of it like gift wrapping. The gift inside stays the same. The wrapper adds presentation. You can use different wrapping paper on the same gift.
import React, { useEffect } from 'react';
const withLogging = (WrappedComponent) => {
const EnhancedComponent = (props) => {
useEffect(() => {
console.log(`Component ${WrappedComponent.name || 'Anonymous'} mounted`);
}, []);
return <WrappedComponent {...props} />;
};
EnhancedComponent.displayName = `withLogging(${WrappedComponent.name || 'Anonymous'})`;
return EnhancedComponent;
};
export default withLogging;
Apply it to any component:
import React from 'react';
import withLogging from './withLogging';
const MyComponent = ({ name }) => (
<div>
<h2>Hello, {name}!</h2>
</div>
);
export default withLogging(MyComponent);
MyComponent doesn't know it's being logged. withLogging adds the behavior transparently. You can stack HOCs — withLogging(withAuth(MyComponent)) — to compose multiple behaviors.
When HOCs Work Well
- Cross-cutting concerns. Logging, analytics, auth guards, error boundaries.
- Conditional rendering. Show a loading spinner while data loads.
- Prop injection. Add theme, locale, or user data to components automatically.
When They Don't
- Prop collisions. If the HOC injects a prop with the same name as an existing prop, things break silently.
- Ref forwarding. HOCs don't pass refs by default. You need
React.forwardRef, which adds boilerplate. - Wrapper hell. Stack five HOCs and your React DevTools shows five anonymous wrappers. Debugging becomes painful.
- Hooks replaced most use cases.
useEffectfor logging,useContextfor auth — custom hooks solve the same problems with less indirection.
The benefit: Reusable behavior without modifying components. Composition is explicit. Works with any component, class or functional.
The cost: Indirection. The enhanced component's behavior isn't visible in its source code — you have to know it's wrapped. Stacking multiple HOCs creates deeply nested wrapper trees. TypeScript types get messy with multiple HOCs.
I still use HOCs for library-level wrappers and cases where hooks aren't an option (wrapping third-party class components). For most new code, custom hooks are simpler and more explicit.