HOCs: Wrap Components to Enhance Behavior and Logic
A higher-order component (HOC) is a function that takes a component and returns an enhanced version of it. Rather than modifying the component itself, an HOC wraps it and injects new props, state, or behavior. Common use cases: injecting theme data, enforcing authentication, fetching data before rendering, or adding logging and performance tracking. HOCs are how many production libraries (Redux, React Router) integrate with your components.
I spent two years refactoring a codebase that wrapped authentication checks inside every component; a single HOC eliminated that duplication and made security audits trivial.
The Basic HOC Pattern
Creating a Simple HOC
An HOC is a function that receives a component and returns a new component:
// A simple HOC that injects theme props
const withTheme = (Component) => {
return (props) => {
const [theme, setTheme] = React.useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
// Return the wrapped component with theme props injected
return (
<Component
{...props}
theme={theme}
onToggleTheme={toggleTheme}
/>
);
};
};
// Use the HOC to wrap a component
function Button({ theme, onToggleTheme, children }) {
return (
<button
style={{
background: theme === 'light' ? '#fff' : '#333',
color: theme === 'light' ? '#000' : '#fff',
}}
onClick={onToggleTheme}
>
{children}
</button>
);
}
const ThemedButton = withTheme(Button);
// The wrapped component works just like the original
export default function App() {
return <ThemedButton>Toggle Theme</ThemedButton>;
}
The HOC wraps Button, manages theme state, and passes theme and onToggleTheme down. To Button, they're just props.
Display Name for Debugging
A wrapped component loses its display name in React DevTools, making debugging harder:
// Always set displayName for debugging
function withTheme(Component) {
function WithTheme(props) {
// ... theme logic ...
return <Component {...props} theme={theme} />;
}
WithTheme.displayName = `WithTheme(${Component.displayName || Component.name})`;
return WithTheme;
}
// In DevTools, you'll see "WithTheme(Button)" instead of just "WithTheme"
Real-World: Authentication HOC
A classic use case is enforcing authentication:
// HOC that requires authentication
const withAuth = (Component) => {
return (props) => {
const [user, setUser] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
// Check if user is logged in
fetch('/api/me')
.then(res => res.json())
.then(data => {
setUser(data.user);
setIsLoading(false);
})
.catch(() => {
setUser(null);
setIsLoading(false);
});
}, []);
if (isLoading) {
return <div>Authenticating...</div>;
}
if (!user) {
return (
<div>
Please log in to access this page.
<a href="/login">Go to login</a>
</div>
);
}
// User is authenticated, render the component
return <Component {...props} user={user} />;
};
};
// Wrap any component that requires authentication
function Dashboard({ user }) {
return <div>Welcome, {user.name}</div>;
}
const ProtectedDashboard = withAuth(Dashboard);
// Anywhere you use ProtectedDashboard, auth is enforced automatically
export default function App() {
return <ProtectedDashboard />;
}
Now every component you wrap automatically checks authentication. Change the auth logic in one place, and all wrapped components update instantly.
Data-Fetching HOC
Fetch data before rendering a component:
// HOC that fetches data and injects it
const withFetch = (url, dataKey = 'data') => {
return (Component) => {
return (props) => {
const [data, setData] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(true);
const [error, setError] = React.useState(null);
React.useEffect(() => {
let isMounted = true;
fetch(url)
.then(res => res.json())
.then(result => {
if (isMounted) {
setData(result);
setIsLoading(false);
}
})
.catch(err => {
if (isMounted) {
setError(err.message);
setIsLoading(false);
}
});
return () => { isMounted = false; };
}, [url]);
// Inject the fetched data under the specified key
const injectedProps = {
...props,
[dataKey]: data,
isLoading,
error,
};
return <Component {...injectedProps} />;
};
};
};
// Usage: inject posts data into a component
function PostsList({ posts, isLoading, error }) {
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{posts?.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
const PostsWithData = withFetch('/api/posts', 'posts')(PostsList);
export default function App() {
return <PostsWithData />;
}
This HOC turns any component into a data-fetching component without modifying the component itself.
HOC Comparison: Wrapping vs. Alternatives
| Approach | Strength | Weakness |
|---|---|---|
| HOC | Clear separation of concerns; composable | Wrapper hell with many HOCs; static typing harder |
| Render Props | Explicit at the call site; flexible | Nested callbacks can be hard to read |
| Custom Hook | Simplest syntax in modern React; no wrapper | Not for enhancing component output, only logic |
| Context | Clean for app-wide data | Global state can be hard to trace |
Key Takeaways
- A higher-order component is a function that accepts a component and returns an enhanced version of it.
- HOCs inject new props, state, or behavior without modifying the wrapped component—useful for authentication, themes, data fetching, and logging.
- Always set
displayNameon the returned component for better debugging in React DevTools. - HOCs can be composed, but deep nesting (wrapper hell) reduces readability—consider hooks or render props for some use cases.
- Wrap at module-level (not inside render functions) to avoid unnecessary re-wrapping on every render.
Frequently Asked Questions
Why would I use an HOC instead of a hook?
HOCs are for enhancing component output or enforcing behavior before a component renders. Hooks are for extracting logic. If you need to conditionally prevent rendering (like auth), an HOC is clearer. If you just need shared logic, a hook is simpler.
Can I use HOCs with hooks?
Yes. An HOC can return a functional component that uses hooks. Hooks and HOCs solve different problems and work well together.
What happens if I compose HOCs deeply?
Composing too many HOCs (e.g., withTheme(withAuth(withFetch(...))) creates "wrapper hell"—hard to debug and trace props. Limit to 3-4 levels; for more, consider combining logic into fewer HOCs or using context.
Do HOCs affect static properties?
Yes. Static methods on the original component don't automatically copy to the wrapped component. Use the hoist-non-react-statics package to copy them: hoistNonReactStatics(Wrapped, Original).
Should I use HOCs in new projects?
Hooks are preferred for new logic extraction. However, HOCs are still the best pattern for high-order composition tasks (like rendering-gate checks). Most new projects mix both strategies.