What is HOC (higher-order component) in React
HOC (Higher-Order Component) is a function that takes a React component and returns a new component with added behavior, without touching the original.
Theory
TL;DR
- HOC = a function that takes a component and returns a new, enhanced one
- Analogy: pass a plain blender into the HOC, get back one that auto-measures ingredients before blending
- Main point: adds behavior through composition, not by modifying source code
- Used in: Redux
connect, React RouterwithRouter, Material-UIwithStyles - Decision rule: use HOC for cross-cutting concerns in class-heavy codebases; prefer custom hooks in new functional code
Quick example
// withLoading: adds a spinner, no changes to UserList needed
const withLoading = (WrappedComponent) => (props) => {
if (props.isLoading) return <div>Loading...</div>; // intercepts before render
return <WrappedComponent {...props} />; // passes all props through
};
const UserList = ({ users }) => (
<ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
);
const UserListWithLoading = withLoading(UserList);
// <UserListWithLoading isLoading={false} users={[{id:1, name:'Alice'}]} />
// Output: <ul><li>Alice</li></ul>withLoading knows nothing about UserList. It checks one prop and delegates the rest. That separation is the whole point.
Key difference: composition vs inheritance
HOC is composition. You wrap a component externally, preserve its original render method, and inject extra props or logic via closure. The original component stays unchanged. Inheritance reaches inside and modifies the class, which breaks in React's concurrent mode and makes code harder to trace. With HOCs you can stack layers, remove them, and test each one in isolation.
When to use
- Shared data fetching across many components -> HOC like
withDataFetch - Auth checks in a class-based codebase -> HOC; in new functional code, a custom hook is cleaner
- Render-time logging or telemetry -> HOC wraps the render call directly, no hook overhead
- Logic that only one component needs -> skip the HOC, inline it
How React handles a HOC internally
React treats the returned function as a standard component. On mount, it runs the outer HOC function, creating a closure over WrappedComponent. The wrapper captures props in its own lifecycle and delegates render to <WrappedComponent /> via React.createElement. No special browser APIs are involved. It is plain JavaScript function composition that runs during reconciliation.
Common mistakes
Mutating the wrapped component directly:
// Wrong: modifies the original component
const badHOC = (WC) => {
WC.prototype.newMethod = () => {}; // pollutes the source
return WC;
};This breaks component purity and fails in StrictMode, which double-mounts components to catch side effects. Return a new component. Never modify the one you received.
Skipping displayName:
// Wrong: React DevTools shows "Anonymous"
const withData = (WC) => (props) => <WC {...props} />;// Correct
const withData = (WC) => {
const Enhanced = (props) => <WC {...props} />;
Enhanced.displayName = `withData(${WC.displayName || WC.name})`;
return Enhanced;
};Without displayName, your DevTools stack shows Anonymous^12. Debugging becomes a guessing game.
Defining HOC inside render:
// Wrong: new HOC instance on every render
function App() {
const UserListWithLogger = withLogger(UserList); // remounts every render
return <UserListWithLogger users={data} />;
}The wrapped component unmounts and remounts each render, losing all local state. Define HOCs at module level, outside any component.
Losing static methods: When you wrap a component, static methods on the original vanish. Copy them manually:
EnhancedComponent.staticMethod = WrappedComponent.staticMethod;Real-world usage
- Redux:
connect(mapStateToProps)(Component)subscribes to the store and maps state to props - React Router v5:
withRouterinjectslocation,history,matchinto class components - Material-UI v4:
withStylesandwithThemeattach CSS-in-JS styles - Composing multiple HOCs:
withAuth(withData(withLogger(Dashboard)))orcompose(withAuth, withData, withLogger)(Dashboard)using lodashflowRight
Follow-up questions
Q: Write a HOC that fetches data and passes it as a prop.
A: See the withFetch example below. A senior answer adds error handling, a loading state, and an AbortController to cancel the request on unmount.
Q: How do you compose multiple HOCs?
A: withAuth(withData(Component)) or compose(withAuth, withData)(Component). The outermost HOC runs first during render.
Q: Why don't developers use HOCs as much anymore?
A: Hooks solve the same cross-cutting concerns without wrapper stacks, prop collision issues, or ref forwarding complexity. On React 16.8+, a custom hook is almost always the simpler path.
Q: HOC vs render props - what is the difference?
A: HOC injects props automatically and returns an enhanced component; the data flow is implicit. Render props expose data through a function passed as children, which is explicit. Both work; render props give finer control over what gets rendered.
Q: How do you test a HOC-wrapped component?
A: Test the HOC and the wrapped component separately. Mock the HOC's dependencies, shallow-render the wrapper, and assert which props were injected. Or pull the logic into a custom hook and unit-test the hook directly.
Q: How do refs behave inside a HOC? (Senior)
A: Refs don't pass through automatically because ref is not a regular prop. Use React.forwardRef inside the HOC to forward it to the wrapped component. Without it, ref is null on the outer wrapper and any imperative handle breaks.
Examples
Basic: logger HOC
function withLogger(WrappedComponent) {
function LoggerComponent(props) {
console.log(`[${WrappedComponent.name}] props:`, props); // logs on each render
return <WrappedComponent {...props} />;
}
LoggerComponent.displayName = `withLogger(${WrappedComponent.name})`;
return LoggerComponent;
}
const Hello = ({ name }) => <h1>Hello, {name}</h1>;
const HelloWithLogger = withLogger(Hello);
// <HelloWithLogger name="Alice" />
// Console: [Hello] props: { name: 'Alice' }
// Output: <h1>Hello, Alice</h1>No state, no side effects. The HOC wraps and logs. Notice displayName is set so DevTools shows withLogger(Hello), not Anonymous.
Intermediate: auth protection HOC
import { useContext } from 'react';
import { Navigate } from 'react-router-dom';
const withAuth = (WrappedComponent) => {
function Authenticated(props) {
const { user } = useContext(AuthContext); // hooks inside HOC work fine (React 16.8+)
if (!user) return <Navigate to="/login" />; // redirect if not logged in
return <WrappedComponent {...props} user={user} />; // inject user prop
}
Authenticated.displayName = `withAuth(${WrappedComponent.displayName || WrappedComponent.name})`;
return Authenticated;
};
const Dashboard = ({ user }) => <div>Welcome, {user.name}!</div>;
const ProtectedDashboard = withAuth(Dashboard);
// <ProtectedDashboard /> with no user -> redirects to /login
// <ProtectedDashboard /> with user -> "Welcome, Alice!"This pattern matches what react-redux-auth-wrapper does internally. Dashboard itself knows nothing about auth logic. The HOC owns that decision entirely.
Advanced: data fetching HOC with cleanup
import { useState, useEffect } from 'react';
function withFetch(WrappedComponent, url) {
function ComponentWithData(props) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController(); // cancel fetch on unmount
fetch(url, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(setData)
.catch((err) => {
if (err.name !== 'AbortError') setError(err.message);
})
.finally(() => setLoading(false));
return () => controller.abort(); // cleanup on unmount
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <WrappedComponent {...props} data={data} />;
}
ComponentWithData.displayName = `withFetch(${WrappedComponent.displayName || WrappedComponent.name})`;
return ComponentWithData;
}
const UserList = ({ data }) => (
<ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>
);
const UserListWithData = withFetch(UserList, '/api/users');
// <UserListWithData /> -> fetches /api/users, shows loading state, then the listThe AbortController is what separates a production HOC from a demo. I've seen this omission cause memory warnings in dashboard apps with frequent route changes. Without it, a fetch completing after unmount tries to call setData on a dead component. You get a warning and a leak.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.