#Higher-order components
A higher-order component (HOC) is a function that takes a component and returns a new component that renders the original one with something added: extra props, a check before rendering, data, a wrapper. The name mirrors higher-order functions — functions that take or return functions — and the idea is the same: withX(Component) returns an enhanced Component.
A good mental model is gift wrapping. The present (your component) doesn't change; the HOC wraps it in a layer that can decide whether to hand it over at all (an auth check), what to hand over with it (extra props), or what to show while you wait (a loading state). The person receiving it (the parent) only sees the wrapped box.
HOCs were the way to share behaviour between components before hooks (2015–2019): connect() from react-redux, withRouter from React Router 4/5, withStyles from Material-UI v4. You'll still meet them in older codebases and in interviews, and a few legitimately remain (React.memo is a HOC, and so is withErrorBoundary in react-error-boundary). For new shared logic, hooks have replaced them — see HOCs vs render props vs hooks.
From my notes — the extraProp example, step by step:
// Step 1: a normal component. It expects extraProp, but doesn't know where it comes from.
export const MessageComponent = ({ message, extraProp }) => {
return (
<div>
<h2>{message}</h2>
<p>{extraProp}</p>
</div>
);
};
// Step 2: the HOC. Takes a component, returns a new one that adds a prop.
export const withExtraProp = (WrappedComponent) => {
return (props) => {
const extraProp = "This is an extra prop from HOC!";
return <WrappedComponent {...props} extraProp={extraProp} />;
};
};
// Step 3: enhance once, at module level.
export const EnhancedMessageComponent = withExtraProp(MessageComponent);
// Step 4: render it. The caller passes only message.
export const App = () => {
return (
<div>
<EnhancedMessageComponent message="Hello from HOC!" />
</div>
);
};- When the module loads,
withExtraProp(MessageComponent)runs once. It doesn't render anything; it just returns a new function component,(props) => …, which closes overWrappedComponent(hereMessageComponent). That new component isEnhancedMessageComponent. - React renders
App, which renders<EnhancedMessageComponent message="Hello from HOC!" />. React calls the inner arrow function withprops = { message: "Hello from HOC!" }. - The wrapper computes
extraPropand returns<MessageComponent {...props} extraProp="This is an extra prop from HOC!" />. The spread forwards everything the caller passed;extraPropis added on top (and would override a caller'sextraProp, because it comes later). MessageComponentrenders an<h2>with "Hello from HOC!" and a<p>with the injected string. The test checks both texts.- The caller never mentioned
extraProp. That's the HOC idea: behaviour attached from outside, with the wrapped component unaware of where its props came from.
The authentication HOC
From my notes: "HOC great example" — check whether the user is signed in before rendering something. My notes hard-coded isAuthenticated = true with a comment; here it reads a real context so the example actually runs (both branches are tested):
import { createContext, useContext } from "react";
export const AuthContext = createContext({ user: null });
export const withAuth = (WrappedComponent) => {
function WithAuth(props) {
const { user } = useContext(AuthContext);
if (!user) {
return <div>Please log in to access this content.</div>;
}
// If authenticated, render the wrapped component with its original props
return <WrappedComponent {...props} />;
}
WithAuth.displayName = `withAuth(${WrappedComponent.displayName || WrappedComponent.name})`;
return WithAuth;
};
const SecretComponent = () => <h1>Secret Content: You can only see this if you are logged in.</h1>;
export const ProtectedComponent = withAuth(SecretComponent);withAuth(SecretComponent)runs once at module load and returnsWithAuth, a named function component. ItsdisplayNamebecomeswithAuth(SecretComponent), which is what React DevTools and error stacks show instead of "Anonymous".- When
<ProtectedComponent />renders,WithAuthreads the current user fromAuthContext— HOCs can call hooks because the returned component is an ordinary function component. - With no provider (or
user: null), it returns the "Please log in" message and never rendersSecretComponentat all. - Inside
<AuthContext value={{ user: { name: "Rohit" } }}>, the check passes and it renders<SecretComponent {...props} />— the secret heading appears. The test renders both cases. - The same
withAuthcan wrap any number of pages. That reuse was the selling point in my notes: "This HOC can be reused across multiple components that require authentication."
A data-fetching HOC
My notes listed "Fetching data HOC" as a real-world example but had no code for it. Here's one. It's configured first, then applied (withData(url)(Component)), the same two-step shape as react-redux's connect(mapState)(Component):
import { useEffect, useState } from "react";
export const withData = (url, propName = "data") => (WrappedComponent) => {
function WithData(props) {
const [state, setState] = useState({ loading: true, data: null, error: null });
useEffect(() => {
let cancelled = false;
fetch(url)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then((data) => !cancelled && setState({ loading: false, data, error: null }))
.catch((error) => !cancelled && setState({ loading: false, data: null, error }));
return () => {
cancelled = true;
};
}, []);
if (state.loading) return <p>Loading…</p>;
if (state.error) return <p role="alert">Failed: {state.error.message}</p>;
return <WrappedComponent {...props} {...{ [propName]: state.data }} />;
}
WithData.displayName = `withData(${WrappedComponent.displayName || WrappedComponent.name})`;
return WithData;
};
const UserNames = ({ users, title }) => (
<section>
<h2>{title}</h2>
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
</section>
);
export const UsersWithData = withData("/api/users", "users")(UserNames);withData("/api/users", "users")returns a function that takes a component; calling that withUserNamesreturnsWithData. The URL and prop name are captured in closures, so each enhanced component remembers its own configuration.- On first render
state.loadingistrue, so it shows "Loading…". After the commit, the effect starts thefetch. - When the JSON arrives,
setState({ loading: false, data, … })re-renders. Now it renders<UserNames title="Team" users={[…]} />— the computed key{ [propName]: state.data }is how the data lands in a prop calledusers. - If the response isn't OK, the thrown
Error("HTTP 500")lands incatchand the component shows "Failed: HTTP 500". Thecancelledflag stops a late response from setting state after unmount. - The test stubs
fetchfor both outcomes. Compare this with useFetch: a data-fetching hook: the same logic, but a hook returns the data to the component instead of wrapping it.
The classic list of things a well-behaved HOC has to handle, and how React 19 changes it:
- displayName: set it as above, or every wrapped component shows up as
Anonymous/propsin DevTools. - Static methods aren't copied: if
Page.loadDataexists,withAuth(Page).loadDataisundefined. Libraries usedhoist-non-react-staticsto copy them; with hooks this rarely matters any more. - Refs: in React 18 and earlier,
refwasn't a prop, so<Enhanced ref={r} />pointed at the wrapper (or warned for function components) unless the HOC usedforwardRef. In React 19,refis a normal prop and{...props}forwards it automatically. - Prop collisions: two HOCs that both inject
datasilently overwrite each other — the later spread wins. Configurable prop names (likepropNameabove) are the usual defence. - Wrapper hell:
withRouter(connect(map)(withStyles(styles)(withAuth(Page))))produced four extra layers in the component tree, and it was hard to see which layer provided which prop. This is the main reason hooks won.