Higher-Order Components (HOC)
The Pattern That Shaped React Architecture
LinkedIn Hook
Higher-Order Components were once the backbone of every serious React codebase. Redux's connect(), React Router's withRouter(), authentication wrappers, theme injectors — all built on one idea: a function that takes a component and returns an enhanced component.
Then hooks arrived, and most developers abandoned HOCs overnight. They stopped learning the pattern entirely. But here is the problem: HOCs never disappeared from production codebases. They still live in enterprise applications, legacy systems, and even modern libraries.
In interviews, candidates get asked: "What is a Higher-Order Component?" Most give a textbook definition. Then the interviewer asks: "Can you write a withAuth HOC from scratch?" or "When would you use an HOC instead of a custom hook?" and the candidate stalls.
The truth is, understanding HOCs is not about nostalgia. It is about understanding React's composition model at a fundamental level. HOCs teach you how to separate cross-cutting concerns from UI logic. They teach you why React moved to hooks. And they remain the right tool in specific situations that hooks cannot solve cleanly.
In this lesson, I break down what HOCs really are, build a withAuth example step by step, explain why hooks replaced most HOC use cases, and show you the scenarios where HOCs still make more sense than any hook.
If you have ever wrapped a component in connect() or withRouter() without understanding the pattern underneath — this lesson fills that gap.
Read the full lesson -> [link]
#React #JavaScript #InterviewPrep #Frontend #HigherOrderComponents #DesignPatterns #WebDevelopment #CodingInterview #100DaysOfCode
What You'll Learn
- What a Higher-Order Component is and how the pattern works at a fundamental level
- How to build a practical withAuth HOC from scratch
- Why HOCs were the dominant pattern before hooks and what problems they solved
- How HOCs compare to custom hooks and when each approach is appropriate
- When HOCs still make sense in modern React applications
The Concept — Enhancing Without Modifying
Analogy: The Gift Wrapping Service
Imagine you run an online store that sells products — books, electronics, clothing. Each product is great on its own. But during the holiday season, customers want gift wrapping. You have two options.
Option one: go into the factory and modify every single product to include wrapping paper, a bow, and a gift tag built into the product itself. The book now has wrapping paper glued to its cover. The laptop ships with a bow permanently attached. Every product changes internally. This is messy, and non-gift customers get wrapping they never wanted.
Option two: create a gift wrapping station. Any product passes through this station and comes out wrapped — same product inside, but now it has wrapping paper, a bow, and a gift tag on the outside. The product itself never changes. The wrapping station works with any product: books, electronics, clothing, anything.
That is exactly what a Higher-Order Component does. The product is your original component. The gift wrapping station is the HOC — a function that takes any component, wraps it with extra behavior (authentication checks, data fetching, logging, theme injection), and returns an enhanced version. The original component never changes. It does not even know it has been wrapped.
withAuth is a wrapping station that checks if the user is logged in before showing the product. withTheme is a station that attaches color information. connect() from Redux is a station that attaches store data. The product — your component — just receives props and renders. The wrapping station handles the rest.
What Is a Higher-Order Component?
A Higher-Order Component is a function that takes a component as an argument and returns a new component with additional behavior. It is not a React API or a special syntax — it is a pattern that emerges from the fact that React components are just functions (or classes) and can be passed around like any other value.
Code Example 1: The HOC Pattern — Bare Bones
// A Higher-Order Component is a FUNCTION
// It takes a component as input and returns a NEW component as output
// The new component wraps the original and adds extra behavior
function withLogger(WrappedComponent) {
// Return a new component (this is the "enhanced" version)
return function EnhancedComponent(props) {
// Added behavior: log every render
console.log(`[Logger] ${WrappedComponent.name} rendered with props:`, props);
// Render the original component, passing ALL props through
// The original component has no idea it is being wrapped
return <WrappedComponent {...props} />;
};
}
// A simple component — knows nothing about logging
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
// Create the enhanced version by passing the component through the HOC
const GreetingWithLogger = withLogger(Greeting);
// Usage — use the enhanced version exactly like the original
function App() {
return <GreetingWithLogger name="Alice" />;
}
// Output in console:
// [Logger] Greeting rendered with props: { name: "Alice" }
//
// Output on screen:
// Hello, Alice!
Key point: The HOC does not modify the original component. It creates a new component that wraps it. The original Greeting still works on its own without any logging. The HOC adds behavior from the outside.
The Classic Use Case — withAuth
Authentication gating is the most common HOC example, and the one interviewers expect you to build. The idea: you have pages that require login. Instead of putting auth checks inside every page component, you create one HOC that handles it for all of them.
Code Example 2: Building a withAuth HOC
import { useContext } from "react";
import { Navigate } from "react-router-dom";
// Assume we have an AuthContext that provides user info
const AuthContext = React.createContext(null);
// The withAuth HOC — protects any component from unauthenticated access
function withAuth(WrappedComponent) {
return function AuthenticatedComponent(props) {
// Access the auth state from context
const user = useContext(AuthContext);
// If no user is logged in, redirect to the login page
// The wrapped component never renders — the user never sees it
if (!user) {
return <Navigate to="/login" replace />;
}
// User is authenticated — render the wrapped component
// Pass the user object as a prop along with all original props
return <WrappedComponent {...props} user={user} />;
};
}
// Page components — they do NOT handle auth logic themselves
// They simply expect a "user" prop and render their content
function Dashboard({ user }) {
return <h1>Welcome back, {user.name}! Here is your dashboard.</h1>;
}
function Settings({ user }) {
return <h1>Settings for {user.email}</h1>;
}
function AdminPanel({ user }) {
return <h1>Admin Panel — Role: {user.role}</h1>;
}
// Wrap each page with auth protection — one line per page
const ProtectedDashboard = withAuth(Dashboard);
const ProtectedSettings = withAuth(Settings);
const ProtectedAdmin = withAuth(AdminPanel);
// Usage in the router
function App() {
return (
<AuthContext.Provider value={{ name: "Alice", email: "alice@dev.io", role: "admin" }}>
<Routes>
<Route path="/dashboard" element={<ProtectedDashboard />} />
<Route path="/settings" element={<ProtectedSettings />} />
<Route path="/admin" element={<ProtectedAdmin />} />
</Routes>
</AuthContext.Provider>
);
}
// When user is logged in and visits /dashboard:
// Output: "Welcome back, Alice! Here is your dashboard."
//
// When user is NOT logged in and visits /dashboard:
// Output: Redirected to /login — Dashboard never renders
//
// Key benefit: Dashboard, Settings, and AdminPanel
// have ZERO authentication logic inside them
When HOC Was King (Pre-Hooks Era)
Before React 16.8 introduced hooks, class components were the standard. Class components could not share stateful logic easily. If two components needed the same behavior — subscribing to a data source, checking auth, tracking window size — you had to duplicate the logic or use one of two patterns: HOCs or render props.
HOCs became the dominant solution. The entire React ecosystem was built on them:
- Redux:
connect(mapStateToProps, mapDispatchToProps)(Component)— injected store data as props - React Router:
withRouter(Component)— injected location, history, and match as props - Material UI:
withStyles(styles)(Component)— injected CSS classes as props - Relay:
createFragmentContainer(Component, graphqlFragment)— injected GraphQL data as props
Every library exposed HOCs as its primary API. Developers routinely composed multiple HOCs together, wrapping a component three, four, even five layers deep.
Code Example 3: The HOC Composition Problem (Wrapper Hell)
// Pre-hooks era: a typical component needing multiple behaviors
// Each HOC wraps the previous one — creating deeply nested wrappers
// This is what a real component looked like in 2017-2018
class UserProfile extends React.Component {
render() {
// All these props are injected by different HOCs
const { user, theme, dispatch, location, intl } = this.props;
return (
<div style={{ background: theme.background }}>
<h1>{intl.formatMessage({ id: "profile.title" })}</h1>
<p>{user.name}</p>
<p>Current path: {location.pathname}</p>
</div>
);
}
}
// The HOC wrapping chain — each one adds props
// Read from inside out: UserProfile gets wrapped by each HOC in sequence
export default withRouter( // adds: location, history, match
withStyles(styles)( // adds: classes (CSS)
withTheme( // adds: theme
injectIntl( // adds: intl (internationalization)
connect( // adds: user, dispatch (Redux)
mapStateToProps,
mapDispatchToProps
)(UserProfile)
)
)
)
);
// Problems with this approach:
// 1. WRAPPER HELL: React DevTools shows 5 nested wrapper components
// <withRouter(withStyles(withTheme(injectIntl(Connect(UserProfile)))))>
//
// 2. PROP COLLISION: What if two HOCs inject a prop with the same name?
// Both withRouter and your Redux mapStateToProps might inject "location"
//
// 3. UNCLEAR ORIGIN: Looking at UserProfile, where does "theme" come from?
// You have to trace through the HOC chain to find out
//
// 4. STATIC TYPING: TypeScript/Flow struggle to infer types through HOC chains
HOC vs Custom Hooks — The Modern Comparison
Hooks solved the problems that made HOC composition painful. A custom hook lets you extract and share stateful logic without wrapping components. But HOCs and hooks solve problems differently, and each has strengths.
Code Example 4: Same Feature, Two Approaches
// ============================================
// APPROACH 1: HOC (withWindowSize)
// ============================================
function withWindowSize(WrappedComponent) {
return function WithWindowSize(props) {
const [size, setSize] = React.useState({
width: window.innerWidth,
height: window.innerHeight,
});
React.useEffect(() => {
const handleResize = () =>
setSize({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
// Inject the size data as props
return <WrappedComponent {...props} windowSize={size} />;
};
}
// Usage: wrap the component
const ResponsiveNav = withWindowSize(function Nav({ windowSize }) {
return windowSize.width < 768 ? <MobileMenu /> : <DesktopMenu />;
});
// ============================================
// APPROACH 2: Custom Hook (useWindowSize)
// ============================================
function useWindowSize() {
const [size, setSize] = React.useState({
width: window.innerWidth,
height: window.innerHeight,
});
React.useEffect(() => {
const handleResize = () =>
setSize({ width: window.innerWidth, height: window.innerHeight });
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
// Return the data directly — no prop injection needed
return size;
}
// Usage: call the hook inside the component
function ResponsiveNav() {
const { width } = useWindowSize();
return width < 768 ? <MobileMenu /> : <DesktopMenu />;
}
// Why the hook version wins for most cases:
// 1. No wrapper component — cleaner React DevTools tree
// 2. No prop name collisions — you destructure exactly what you need
// 3. Explicit data flow — you can see where "width" comes from in the component
// 4. Composable — call multiple hooks without nesting
// 5. TypeScript loves it — return types are straightforward
When HOCs Still Make Sense
Hooks replaced HOCs for most use cases, but there are specific scenarios where HOCs remain the better tool:
-
Conditional rendering of entire components — When you need to completely prevent a component from rendering (like auth gating), an HOC keeps that logic entirely outside the component. The component code never executes at all if the condition fails.
-
Wrapping third-party components — When you cannot modify a component (it comes from a library), an HOC lets you add behavior without touching its source code.
-
Static composition at module level — HOCs compose at the module level (
export default withAuth(Page)), which means the wrapping happens once at definition time, not on every render. This can be useful for configuration-style patterns. -
Legacy codebases — Many production applications built before hooks still use HOCs extensively. Understanding and maintaining them is a real-world skill.
Common Mistakes
Mistake 1: Calling the HOC inside the render function
// BAD: Creating the enhanced component INSIDE the render
// This creates a NEW component type on every render
// React unmounts and remounts it every time — destroying all state
function App() {
// withAuth(Dashboard) runs every render, creating a new component reference
const ProtectedDashboard = withAuth(Dashboard);
return <ProtectedDashboard />;
// Every render: previous instance unmounts, new instance mounts
// All state, effects, and DOM are destroyed and recreated
}
// GOOD: Create the enhanced component OUTSIDE the component — once
const ProtectedDashboard = withAuth(Dashboard);
function App() {
return <ProtectedDashboard />;
// Same component reference every render — state is preserved
}
Mistake 2: Not forwarding props to the wrapped component
// BAD: The HOC consumes props but does not pass them through
function withLoading(WrappedComponent) {
return function WithLoading({ isLoading }) {
if (isLoading) return <p>Loading...</p>;
// Only passes isLoading — all other props are LOST
return <WrappedComponent />;
};
}
// Usage: title prop disappears
const EnhancedCard = withLoading(Card);
<EnhancedCard isLoading={false} title="Hello" />;
// Card receives NO props — title is gone
// GOOD: Spread all remaining props to the wrapped component
function withLoading(WrappedComponent) {
return function WithLoading({ isLoading, ...rest }) {
if (isLoading) return <p>Loading...</p>;
// Spread the rest of the props so nothing is lost
return <WrappedComponent {...rest} />;
};
}
// Now Card receives { title: "Hello" } as expected
Mistake 3: Not forwarding refs
// BAD: Refs do not pass through HOCs by default
// If a parent tries to ref the enhanced component, it gets the wrapper, not the inner component
function withTooltip(WrappedComponent) {
return function WithTooltip(props) {
return <WrappedComponent {...props} />;
};
}
const EnhancedInput = withTooltip(Input);
const ref = useRef();
<EnhancedInput ref={ref} />;
// ref.current points to WithTooltip wrapper, NOT the Input element
// GOOD: Use React.forwardRef to pass the ref through
function withTooltip(WrappedComponent) {
const WithTooltip = React.forwardRef(function WithTooltip(props, ref) {
return <WrappedComponent {...props} ref={ref} />;
});
// Set a display name for better DevTools readability
WithTooltip.displayName = `withTooltip(${WrappedComponent.displayName || WrappedComponent.name})`;
return WithTooltip;
}
// Now ref.current correctly points to the underlying Input element
Interview Questions
Q: What is a Higher-Order Component in React?
A Higher-Order Component is a function that takes a component as its argument and returns a new component with additional behavior. It is a composition pattern, not a React API. The HOC wraps the original component, can inject props, add conditional rendering, or perform side effects before or after rendering the wrapped component. The original component is not modified — the HOC creates a new wrapper around it. Common examples include withAuth for authentication gating, connect() from Redux for injecting store data, and withRouter from React Router for injecting navigation props.
Q: Write a withAuth HOC that redirects unauthenticated users to a login page.
The HOC function takes a component, returns a new component that checks auth state (via context, a hook, or a global store). If the user is not authenticated, it returns a
<Navigate to="/login" />redirect. If authenticated, it renders the wrapped component with all original props plus the user object as an extra prop. The enhanced component is created outside of any render function to avoid remounting issues. Example:const ProtectedDashboard = withAuth(Dashboard)— Dashboard never handles auth logic, it just receives auserprop and renders content.
Q: Why did hooks replace HOCs for most use cases?
HOCs had three major problems: wrapper hell (deeply nested wrapper components in DevTools), prop collision (two HOCs might inject props with the same name), and unclear data origin (you cannot tell where a prop comes from without tracing the HOC chain). Hooks solve all three. A custom hook returns values directly into the component scope — no wrappers, no prop injection, no ambiguity. Multiple hooks compose by calling them sequentially, not by nesting. TypeScript can infer hook return types easily, while HOC chains made type inference extremely difficult.
Q: When would you still use an HOC instead of a custom hook?
HOCs still make sense in four scenarios: (1) when you need to completely prevent a component from rendering based on a condition, like auth gating, where the HOC short-circuits before the component ever executes; (2) when wrapping third-party components that you cannot modify; (3) in legacy codebases built before hooks where refactoring every HOC to hooks is not practical; and (4) for static, module-level composition where the wrapping happens once at definition time. For shared stateful logic, data fetching, subscriptions, or any behavior where the component needs direct access to the values, custom hooks are the better choice.
Q: What is the difference between a Higher-Order Component and a wrapper component?
A Higher-Order Component is a function that creates a wrapper at definition time — you call
withAuth(Dashboard)once and get back a new component. A wrapper component is a component that wraps children at render time — you write<AuthGuard><Dashboard /></AuthGuard>in JSX. Both achieve similar results, but HOCs compose at the module level and inject props automatically, while wrapper components compose in JSX and typically use children or render props. In modern React, wrapper components (especially with children) are generally preferred because they are more explicit and easier to read.
Quick Reference — Cheat Sheet
HIGHER-ORDER COMPONENTS (HOC)
=================================
What it is:
A function that takes a component and returns a new enhanced component
Pattern: const Enhanced = withSomething(OriginalComponent)
NOT an API: It is a composition pattern using plain JavaScript functions
Basic structure:
function withFeature(WrappedComponent) {
return function Enhanced(props) {
// Add behavior here (auth check, data fetch, logging)
return <WrappedComponent {...props} extraProp={value} />;
};
}
Classic HOCs (pre-hooks era):
Redux: connect(mapState, mapDispatch)(Component)
React Router: withRouter(Component)
Material UI: withStyles(styles)(Component)
Relay: createFragmentContainer(Component, fragment)
HOC vs Custom Hook:
+---------------------+---------------------------+---------------------------+
| Aspect | HOC | Custom Hook |
+---------------------+---------------------------+---------------------------+
| Composition | Nesting (wrapper hell) | Sequential (flat) |
| Data flow | Injected as props | Returned directly |
| DevTools | Extra wrapper nodes | No extra nodes |
| Prop collision | Possible | Impossible |
| TypeScript | Hard to type chains | Easy to type |
| Prevents render | Yes (short-circuit) | No (hook runs regardless) |
| Wraps third-party | Yes | No (cannot modify) |
+---------------------+---------------------------+---------------------------+
When HOC still makes sense:
- Auth gating (prevent component from rendering entirely)
- Wrapping third-party components you cannot modify
- Legacy codebases with existing HOC patterns
- Static, module-level composition
Rules for writing HOCs:
1. Create enhanced components OUTSIDE render (avoid remounting)
2. Spread {...props} to forward all props to wrapped component
3. Use React.forwardRef to pass refs through
4. Set displayName for readable DevTools output
5. Do NOT mutate the original component — always wrap it
Common pattern — withAuth:
const ProtectedPage = withAuth(PageComponent)
// If not logged in -> redirect to /login
// If logged in -> render PageComponent with user prop
Convention:
HOC name: withSomething (withAuth, withTheme, withRouter)
Display name: withAuth(ComponentName) — set via .displayName
Previous: Lesson 8.4 — Image & Asset Optimization -> Next: Lesson 9.2 — Render Props Pattern ->
This is Lesson 9.1 of the React Interview Prep Course — 10 chapters, 42 lessons.