Higher-Order Components
What is a HOC?
A Higher-Order Component is a function that takes a component and returns a new component with additional props or behavior. Follows the withX naming convention.
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
const { user, isLoading } = useAuth();
if (isLoading) return <LoadingSpinner />;
if (!user) return <Redirect to="/login" />;
return <WrappedComponent {...props} user={user} />;
};
}
// Usage
const ProtectedDashboard = withAuth(Dashboard);
Common HOC Patterns
withLoading
function withLoading(Component) {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) return <Spinner />;
return <Component {...props} />;
};
}
withErrorBoundary
function withErrorBoundary(Component, FallbackUI) {
return class WithErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
componentDidCatch(error, info) { logError(error, info); }
render() {
if (this.state.hasError) return <FallbackUI />;
return <Component {...this.props} />;
}
};
}
withLogger (Analytics)
function withLogger(Component, eventName) {
return function LoggedComponent(props) {
useEffect(() => {
analytics.track(`${eventName}_mounted`);
return () => analytics.track(`${eventName}_unmounted`);
}, []);
return <Component {...props} />;
};
}
Problems with HOCs
1. Wrapper Hell — Hard to Debug
export default withAuth(withLoading(withLogger(withTheme(Dashboard))));
// DevTools shows: withTheme(withLogger(withLoading(withAuth(Dashboard))))
// Hard to see which HOC is causing issues
2. Props Collision
// Both HOCs inject a `data` prop — later one silently overwrites
const Enhanced = withUserData(withProductData(Component));
// Component receives only one `data`!
3. TypeScript Complexity
Properly typing composed HOCs requires advanced generics.
4. Ref Forwarding
Refs don't automatically work through HOCs — must add React.forwardRef.
Modern Alternative: Custom Hooks
// ❌ HOC approach: wrapper component
function withWindowSize(Component) {
return function(props) {
const size = useWindowSize();
return <Component {...props} windowSize={size} />;
};
}
// ✅ Hook approach: no wrapper, no collision
function Dashboard() {
const { width, height } = useWindowSize();
const { user } = useAuth();
const { theme } = useTheme();
// All explicit, no hidden dependencies
}
When HOCs Still Make Sense
- Error Boundaries — can't implement as hooks yet
- Wrapping third-party components (class-based libraries)
- Legacy class component codebases
- Library implementations (Redux's
connect, React Router'swithRouter)
HOC vs Hook Decision
| Use HOC | Use Hook |
|---|---|
| Error boundaries | Any other logic sharing |
| Wrapping class components | Function components |
| Library code (connect, withRouter) | Application code |
| Adding render behavior | Adding state/effect logic |
Modern best practice (2024+): Hooks for almost everything. HOCs for error boundaries and legacy code.
Content from Frontend-Master-Prep-Series — 03-react