A higher-order component (HOC) in React is a function that takes a component and returns a new component with extra behaviour, for example const UserListWithLoading = withLoading(UserList). HOCs were the main way to reuse logic between class components. Since React 16.8, custom hooks are usually simpler for sharing logic, but HOCs are still useful for wrapping what a component renders and you will meet them in libraries such as React.memo and Auth0’s withAuthenticationRequired.
This guide explains how HOCs work, how to write them correctly in React 19, the pitfalls (including one I reproduced in a test), and when to use a hook instead. All examples were built with React 19.3 and Vite 8.3 and checked in a browser.
How a Higher-Order Component Works
The name comes from higher-order functions, which take or return functions. An HOC doesn’t change the original component; it returns a wrapper that renders it, adding props, conditions or other components around it:
const EnhancedComponent = higherOrderComponent(WrappedComponent);Here are two HOCs: one that shows a placeholder while data loads, and one that logs props on every render:
// withLoading: show a placeholder while isLoading is true
export function withLoading(WrappedComponent) {
function WithLoading({ isLoading, ...props }) {
if (isLoading) return <p>Loading...</p>;
return <WrappedComponent {...props} />;
}
WithLoading.displayName = `withLoading(${getName(WrappedComponent)})`;
return WithLoading;
}
// withLogger: log props on every render (handy while debugging)
export function withLogger(WrappedComponent) {
function WithLogger(props) {
console.log(`[render] ${getName(WrappedComponent)}`, props);
return <WrappedComponent {...props} />;
}
WithLogger.displayName = `withLogger(${getName(WrappedComponent)})`;
return WithLogger;
}
const getName = (C) => C.displayName || C.name || 'Component';Using them:
function UserList({ users }) {
return <ul>{users.map((u) => <li key={u}>{u}</li>)}</ul>;
}
const UserListWithLoading = withLoading(UserList); // create it once, at module level
<UserListWithLoading isLoading={loading} users={['Ada', 'Alan']} />In my test, the component rendered “Loading…” while isLoading was true and the user list after it turned false. Notice that withLoading removes isLoading from the props it passes on, so the wrapped component only receives what it needs.
Writing HOCs Correctly in React 19
1. Create HOCs outside render
This is the most important rule, and the easiest to break. If you call an HOC inside a component, it creates a new component type on every render, so React unmounts the old one and mounts a fresh one, losing all its state:
function App() {
const [tick, setTick] = useState(0);
const UnstableCounter = withLogger(Counter); // DON'T: new component on every render
return (
<>
<StableCounter /> {/* created once at module level */}
<UnstableCounter />
<button onClick={() => setTick(tick + 1)}>Re-render parent</button>
</>
);
}I clicked both counters three times (both showed 3), then re-rendered the parent once. The stable counter still showed 3; the one created inside render reset to 0. Always create wrapped components at the top level of a module.
2. Pass through props, including ref
Spread the remaining props onto the wrapped component ({...props}) so the HOC is transparent. In React 19, ref is a regular prop for function components, so it passes through the spread as well, and you no longer need forwardRef:
// React 19: ref arrives as a normal prop
function TextInput({ ref, ...props }) {
return <input ref={ref} {...props} />;
}
const LoggedInput = withLogger(TextInput);
const inputRef = useRef(null);
<LoggedInput ref={inputRef} placeholder="type here" />
// inputRef.current.focus() focuses the real <input>In my test, calling inputRef.current.focus() focused the actual input inside the HOC. In React 18 and earlier, the same code would not forward the ref; you had to wrap the HOC’s inner component in forwardRef.
3. Set a displayName
Give the wrapper a name such as withLoading(UserList) so it is recognizable in React DevTools and error messages. Note that this mainly helps in development: in my production build, the minifier had renamed the components, and the display names came out as withLoading(_) and withLogger(y).
4. Copy static methods if you rely on them
A wrapper doesn’t automatically have the wrapped component’s static properties. If other code reads statics from the component, copy them explicitly or with the hoist-non-react-statics package.
5. Don’t mutate the wrapped component
Never change the original component inside an HOC (for example by editing its prototype). Return a new wrapper instead, so HOCs can be combined safely.
Composing Several HOCs
const Enhanced = withLogger(withLoading(UserList));
// The outermost HOC renders first:
// withLogger(...) -> withLoading(...) -> UserListDeep stacks of HOCs make the component tree harder to read and props harder to trace (“wrapper hell”), which is one of the main reasons hooks replaced them for most uses.
HOCs vs Custom Hooks
A custom hook shares logic by returning values; an HOC shares rendering by wrapping a component. Here is the same loading behaviour both ways:
// Hook: the component decides what to render
function useUsers() {
const [users, setUsers] = useState(null);
useEffect(() => { fetchUsers().then(setUsers); }, []);
return { users, isLoading: users === null };
}
function UserPage() {
const { users, isLoading } = useUsers();
if (isLoading) return <p>Loading...</p>;
return <UserList users={users} />;
}
// HOC: the wrapper decides what to render
const UserListWithLoading = withLoading(UserList);| Custom hook | Higher-order component | |
|---|---|---|
| Shares | Stateful logic | Rendering behaviour around a component |
| Where the logic is visible | Inside the component | Hidden in the wrapper |
| Prop name collisions | None; you name the returned values | Possible when several HOCs inject props |
| Extra components in the tree | None | One per HOC |
| TypeScript | Simple | Generic typing of injected props can get complex |
| Best for | Data fetching, subscriptions, form state, most reuse | Guarding or wrapping whole components, library APIs |
In new code, reach for a custom hook first. For more on hook-based data loading, see how to fetch data with React hooks.
When HOCs Are Still a Good Choice
- Route or permission guards that decide whether to render a component at all, such as Auth0’s
withAuthenticationRequired(see how to create an Auth0 protected route). - Error boundaries and Suspense wrappers applied to many components, for example
withErrorBoundary(Component). - Library APIs you can’t change, such as
React.memo, which is itself an HOC, orconnect()in older Redux code. - Instrumentation such as logging, analytics or performance tracking applied consistently to many components.
- Class components, which can’t use hooks directly; an HOC can call a hook and pass the result in as a prop.
Common Pitfalls
- Creating HOCs inside render, which resets state on every parent render.
- Not spreading props, so the wrapped component silently loses props.
- Two HOCs injecting a prop with the same name, where one overwrites the other.
- Deeply nested HOCs that make debugging and DevTools hard to follow.
- Using an HOC where a custom hook would be clearer.
Conclusion
A higher-order component is just a function from component to component. Write them at module level, pass all props through (in React 19 that includes ref), give them a display name, and keep stacks shallow. For sharing logic, custom hooks are usually the better tool; keep HOCs for wrapping and guarding whole components, and for the library APIs that still use them.
FAQs
What is a higher-order component in React?
A function that takes a component and returns a new component with added behaviour, such as a loading state, authentication check or logging, without modifying the original component.
Are higher-order components still used in React?
Yes, but less often. Custom hooks have replaced them for sharing logic, while HOCs remain common in libraries (React.memo, withAuthenticationRequired) and for wrapping whole components with guards or error boundaries.
What is the difference between an HOC and a custom hook?
A custom hook shares stateful logic and returns values the component uses. An HOC wraps a component and controls what is rendered around it. Hooks add no extra components to the tree and avoid prop name collisions.
Do I need forwardRef in an HOC in React 19?
No. In React 19, ref is a regular prop for function components, so spreading props in the HOC passes the ref through. In React 18 and earlier you needed forwardRef.
Why does my component lose its state when wrapped in an HOC?
You are probably calling the HOC inside a render function, which creates a new component type every render. Create the wrapped component once, outside any component.
Is React.memo a higher-order component?
Yes. React.memo takes a component and returns a memoized version that skips re-rendering when its props have not changed.








