Higher-Order Components
A Higher-Order Component (HOC) is a function that takes a component as an argument and returns a new component with extra props, behavior, or data. HOCs are not part of the React API itself — they are a design pattern built entirely from JavaScript functions, borrowed from the idea of higher-order functions (functions that take or return other functions). Before hooks existed, HOCs were the primary way to share stateful logic between components. Today custom hooks handle most of these use cases, but HOCs are still used in some libraries (like react-redux‘s connect and route-guarding utilities), and understanding them helps you read older codebases and certain third-party APIs.
Overview / How It Works
The core idea is simple: components transform props into UI, and a HOC transforms a component into another component. In code, a HOC has the shape const EnhancedComponent = withSomething(WrappedComponent). Internally, withSomething is just a JavaScript function that returns a new function component. That new component typically renders the original component, passing through its own props plus whatever extra props or logic the HOC provides.
Why does this pattern exist? In React, logic like “subscribe to a data source,” “check if the user is authenticated,” or “track window size” often needs to be reused across many unrelated components. Before hooks, the only way to add local state or lifecycle behavior was a class component, and classes can’t be mixed together the way functions can. Two common (older) solutions emerged: render props and Higher-Order Components. A HOC wraps a component and injects the shared behavior as props, so the wrapped component stays “dumb” and reusable, while the HOC owns the stateful logic.
Under the hood, nothing magical happens: when React renders <EnhancedComponent />, it is rendering the outer function component created by the HOC. That outer component runs its own hooks (e.g. useState, useEffect) and then returns <WrappedComponent {...allProps} /> from its JSX. React reconciles this like any other nested component tree: the outer component re-renders whenever its own state/props change, and it passes down new props to the inner component, which then re-renders too if those props changed. There’s no special HOC lifecycle — it is ordinary parent–child rendering, just generated by a function instead of written by hand each time.
It’s important to understand that a HOC does not modify the component you pass in. It creates a brand-new component that composes the original. This matters for naming, debugging (React DevTools shows the wrapper’s name), and for the rule that HOCs should never mutate their input component or use inheritance — they should use composition.
Syntax
function withExtraProps(WrappedComponent) {
return function EnhancedComponent(props) {
const extraValue = /* compute or subscribe to something */ null;
return <WrappedComponent extraValue={extraValue} {...props} />;
};
}
const MyEnhancedComponent = withExtraProps(MyComponent);
| Part | Meaning |
|---|---|
withExtraProps |
The HOC itself — a plain function, conventionally named withXxx. |
WrappedComponent |
The component passed in, whose behavior will be enhanced. |
EnhancedComponent |
The new function component returned by the HOC; this is what actually gets rendered. |
{...props} |
Pass-through props so the wrapped component still receives everything the caller gave the enhanced component. |
extraValue |
The new prop, state, or data the HOC injects into the wrapped component. |
Examples
Example 1: withLoading — injecting a loading flag
function withLoading(WrappedComponent) {
return function WithLoading({ isLoading, ...rest }) {
if (isLoading) {
return <p>Loading...</p>;
}
return <WrappedComponent {...rest} />;
};
}
function UserList({ users }) {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
const UserListWithLoading = withLoading(UserList);
function App() {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch("/api/users")
.then((res) => res.json())
.then((data) => {
setUsers(data);
setIsLoading(false);
});
}, []);
return <UserListWithLoading isLoading={isLoading} users={users} />;
}
Renders: While isLoading is true, the text “Loading…”. Once the fetch resolves, it switches to a bullet list of user names.
withLoading takes any list-rendering component and adds a loading guard in front of it, without UserList itself needing to know about loading state. The HOC destructures isLoading off the props so it isn’t accidentally forwarded to WrappedComponent, and passes the rest through with ...rest.
Example 2: withAuth — guarding a component behind authentication
import { useContext } from "react";
import { AuthContext } from "./AuthContext";
function withAuth(WrappedComponent) {
return function WithAuth(props) {
const { user } = useContext(AuthContext);
if (!user) {
return <p>Please log in to view this page.</p>;
}
return <WrappedComponent {...props} user={user} />;
};
}
function Dashboard({ user }) {
return <h2>Welcome back, {user.name}!</h2>;
}
const ProtectedDashboard = withAuth(Dashboard);
Renders: If no user is logged in (via AuthContext), shows “Please log in to view this page.” Otherwise shows “Welcome back, <name>!” and injects the current user object as a prop.
This is a very common real-world HOC use case: guarding routes or sections of a page behind authentication. Notice the HOC itself calls a hook (useContext) — this is fine because WithAuth is a proper function component, so it follows the Rules of Hooks just like any other component.
Example 3: withLogger — logging props on every render
function withLogger(WrappedComponent) {
return function WithLogger(props) {
console.log(`Rendering ${WrappedComponent.name} with props:`, props);
return <WrappedComponent {...props} />;
};
}
function PriceTag({ amount }) {
return <span>${amount.toFixed(2)}</span>;
}
const PriceTagWithLogger = withLogger(PriceTag);
// Usage: <PriceTagWithLogger amount={19.99} />
Output:
Rendering PriceTag with props: { amount: 19.99 }
Renders a <span> showing “$19.99”, and logs the wrapped component’s name and props to the console every time it renders. This pattern is handy for debugging render frequency or unexpected prop changes during development.
How It Works Step by Step
- Definition time: Calling
withLoading(UserList)immediately runs the HOC function once, which defines and returns a new component (WithLoading). No rendering happens yet — you’ve just created a new component definition, the same as writing any function component by hand. - Mount: When JSX renders
<UserListWithLoading isLoading={true} users={[]} />, React creates a fiber forWithLoadingand calls it as a function with those props. Inside, it runs its own logic (state, effects, conditionals) and returns JSX — either an early-return element or<WrappedComponent {...rest} />. - Update: When the parent re-renders and passes new props (e.g.
isLoadingflips tofalse),WithLoadingre-renders, its condition now falls through, and it renders<UserList users={...} />for the first time (or updates the already-mountedUserList, depending on prior renders) with reconciliation deciding whether to reuse or replace the DOM. - Unmount: If
UserListWithLoadingis removed from the tree (e.g. its parent stops rendering it), React unmounts the entire subtree it produced — bothWithLoadingand, if it was being rendered,UserList— running any cleanup functions registered by effects along the way.
Common Mistakes
Mistake 1: Not forwarding all props
function withLoading(WrappedComponent) {
return function WithLoading({ isLoading }) {
if (isLoading) return <p>Loading...</p>;
return <WrappedComponent />; // other props silently dropped!
};
}
Here only isLoading is destructured, and every other prop the caller passed (like users) is discarded because it’s never spread onto WrappedComponent. The wrapped component will render with missing data and no error message explaining why.
function withLoading(WrappedComponent) {
return function WithLoading({ isLoading, ...rest }) {
if (isLoading) return <p>Loading...</p>;
return <WrappedComponent {...rest} />; // everything else passed through
};
}
The fix is to destructure only the props the HOC actually consumes, and spread the remainder (...rest) onto the wrapped component so nothing is silently lost.
Mistake 2: Creating the HOC-wrapped component inside another component’s render
function App() {
// BAD: a brand-new component type is created on every render of App
const EnhancedList = withLoading(UserList);
return <EnhancedList isLoading={false} users={[]} />;
}
Because withLoading(UserList) runs on every render of App, it produces a new component function each time. React treats a changed component type as a completely different element, so it unmounts and remounts EnhancedList on every single render — destroying its internal state and DOM (and losing focus, animations, etc.) even though nothing conceptually changed.
// GOOD: create the enhanced component once, at module scope
const EnhancedList = withLoading(UserList);
function App() {
return <EnhancedList isLoading={false} users={[]} />;
}
Always call the HOC once, outside of any component’s render (at module/file scope), and reuse the resulting component.
Best Practices
- Name HOCs starting with
with(e.g.withAuth,withLoading) so their purpose is obvious at a glance. - Always pass through unrelated props with the spread operator (
{...rest}) so the wrapped component behaves like a normal component to its caller. - Never mutate the
WrappedComponentpassed in — always return a new component that composes it (composition, not modification). - Define enhanced components once, outside of render (module scope), never inside another component’s function body.
- Set a
displayNameon the returned component in larger codebases (e.g.WithAuth.displayName = \`withAuth(${WrappedComponent.name})\`;) so React DevTools shows a readable name instead of a generic one. - Prefer a custom hook over a HOC for new code when you just need to share stateful logic — hooks avoid the extra wrapper component in the tree and are usually easier to read and compose.
- Reserve HOCs for cases where you genuinely need to wrap the rendered output itself (e.g. conditionally render nothing / a fallback / a redirect around a component), which a hook alone cannot do.
Practice Exercises
- Exercise 1: Write a HOC called
withFallbackthat takes a component and a fallback message. If the wrapped component throws or a prop callederroris truthy, render the fallback message instead of the wrapped component. Otherwise render the wrapped component normally. - Exercise 2: Write
withWindowWidth, a HOC that subscribes to the browser’sresizeevent (usinguseEffectanduseStateinside the returned component) and injects awindowWidthprop into the wrapped component. Remember to clean up the event listener when the enhanced component unmounts. - Exercise 3: Take the
withAuthexample from this lesson and rewrite the same behavior as a custom hook calleduseAuthinstead. Compare the two: which version requires fewer components in the React DevTools tree, and which is easier to reuse inside an existing component without adding a wrapper?
Summary
- A Higher-Order Component is a plain JavaScript function that takes a component and returns a new, enhanced component — it is a pattern, not a React API.
- HOCs work through ordinary composition: the returned component renders the original component, passing along existing props plus new ones.
- Common uses include injecting loading states, authentication guards, and logging — anything that needs to wrap or conditionally alter what gets rendered.
- Always spread through unrelated props, and always define the enhanced component once at module scope, not inside another component’s render.
- Modern React favors custom hooks for sharing stateful logic; reach for a HOC mainly when you need to wrap or replace the rendered output itself.
