What are Higher‑Order Components in React?

Higher‑order components appear most often when maintaining React code written before Hooks became the usual way to share stateful behaviour. An HOC takes a component and returns another component with added props or behaviour. It can still be useful, but the wrapper also changes the component tree and can make data flow harder to trace.
What is a Higher‑Order Component?
A Higher‑Order Component (HOC) is a function which takes a component as an argument and returns a new, enhanced version of that component. This pattern allows for the reuse of logic (and extension) across multiple components without duplicating code.
Basic Syntax of an HOC
Here's a rough outline of what an HOC looks like in a basic form:
const withLogger = <P extends object>(WrappedComponent: React.ComponentType<P>) => {
return (props: P) => {
console.log("Rendering component with props:", props);
return <WrappedComponent {...props} />;
};
};It looks a bit ugly with TypeScript, but here, withLogger is a function which wraps a given component and logs its props every time it renders. This is an example of how an HOC can add cross‑cutting concerns (such as logging) to multiple components without modifying their implementation directly.
Why Use Higher‑Order Components?
HOCs help with:
Code Reusability
– Share logic between multiple components without duplicating code.Separation of Concerns
– Keep UI and logic separate by encapsulating behaviour.Enhancing Components
– Modify or extend component functionality dynamically.
Common use cases for HOCs include session‑aware UI, data fetching, logging, and UI enhancement, which I'll go into more detail about below.
Common Use Cases for HOCs
1. Showing Session‑Aware UI
type AuthenticatedViewProps = { isAuthenticated: boolean };
const withAuthenticatedView = <P extends object>(
WrappedComponent: React.ComponentType<P>,
) => {
return ({ isAuthenticated, ...props }: P & AuthenticatedViewProps) => {
return isAuthenticated
? <WrappedComponent {...(props as P)} />
: <p>Please log in</p>;
};
};This HOC uses an isAuthenticated prop, supplied from a server‑validated session, to choose which UI renders. It is a presentation convenience rather than route protection: protected data and actions must validate the session and authorise the request on the server.
2. Fetching Data in an HOC
import * as React from 'react';
const withData = <T, P extends object>(
WrappedComponent: React.ComponentType<P & { data: T }>,
url: string,
) => {
const WithData = (props: P) => {
const [data, setData] = React.useState<T | null>(null);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
const controller = new AbortController();
let active = true;
const load = async () => {
try {
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const value = await response.json() as T;
if (active) setData(value);
} catch {
if (active && !controller.signal.aborted) {
setError('Could not load the data. Please try again later.');
}
}
};
void load();
return () => {
active = false;
controller.abort();
};
}, [url]);
if (error) return <p role="alert">{error}</p>;
if (data === null) return <p>Loading…</p>;
return <WrappedComponent {...props} data={data} />;
};
return WithData;
};
type Product = { id: number; name: string };
const ProductList = ({ data }: { data: Product[] }) => (
<ul>{data.map(product => <li key={product.id}>{product.name}</li>)}</ul>
);
// Create the wrapper once, at module scope.
const Products = withData<Product[], Record<string, never>>(
ProductList,
'/api/products',
);This TypeScript example checks response.ok, handles request or JSON failures, and aborts the request when the effect is cleaned up. The active flag also stops a late result updating an instance that has been cleaned up. It assumes the endpoint returns the stated product shape; as T does not validate a response at runtime. A real application may also need schema validation, retry and cache policies.
Create Products once at module scope, as shown, then render <Products /> where it is needed. Calling withData() inside another component's render creates a new component type on each render, which can reset state, remount the subtree and restart requests. Custom Hooks or a data‑fetching library often express new code more simply.
3. Conditionally Applying Styles
const withTheme = <P extends object>(WrappedComponent: React.ComponentType<P>) => {
return (props: P) => {
const theme = "dark";
return (
<div className={theme}>
<WrappedComponent {...props} />
</div>
);
};
};In this example, the HOC wraps a component in a <div> that applies a theme class. This allows a shared theme or layout rule without changing the wrapped component's implementation. The extra wrapper can affect layout and selectors, so check that it fits the actual markup.
HOCs vs. Hooks: Which Should You Use?
With the introduction of React Hooks, some of the patterns previously implemented with HOCs can now be replaced with imported hooks. However, HOCs are still useful when:
- You need to wrap class components (since hooks are only for functional components).
- The same logic needs to be applied across multiple unrelated components.
- You want to enforce patterns at a higher level of abstraction.
With that said, in most modern React applications, hooks are preferred over HOCs due to their simpler and more readable nature.
Wrapping Up
Key Takeaways
- HOCs are functions that take a component and return an enhanced version of it.
- They promote code reusability, separation of concerns, and component enhancement.
- Common use cases include session‑aware UI, data fetching, and styling.
- Hooks have replaced many HOC use cases in modern React applications.
Keep an HOC when it expresses an existing cross‑cutting concern clearly or when a library API requires one. Prefer a custom Hook for new reusable stateful logic where no wrapper is needed. Understanding both patterns matters mainly because real React applications often contain both generations of code.