React Design Patterns: Higher-Order Components
Oct 19, 2026
Higher-order components, usually called HOCs, are functions that receive a component and return another component. A regular React component returns JSX, but a higher-order component adds one more layer by generating a new component that can contain extra behavior.
You can think of a HOC as a component factory. We give it an existing component, and it gives us a new component with additional functionality. This is useful when several components need the same behavior or when we want to improve an existing component without changing its original code.
In this article, we will begin with a simple HOC that logs props. Then we will use HOCs to fetch a user, edit and save user data, and finally create a generic solution that works with different resources.
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. The returned component can run some logic before rendering the original component.
The basic structure looks like this:
export const withSomething = (Component) => {
return (props) => {
// Add shared behavior here.
return <Component {...props} />;
};
};Component is the original component that we want to wrap. The returned function is the new component created by the HOC. It receives the incoming props and passes them to the original component with {...props}.
HOCs are helpful for two main reasons:
- They allow several components to share the same behavior.
- They add functionality to an existing component without modifying its original code.
Creating a HOC That Logs Props
A small prop-logging HOC is a good place to see the pattern in practice. It receives a component, logs the props passed to the generated component, and then renders the original component with those props.
export const logProps = (Component) => {
return (props) => {
console.log(props);
return <Component {...props} />;
};
};The HOC does not need to change the original component. It only creates a wrapper around it. Every time the wrapper renders, it logs the incoming props before passing them to Component.
Imagine that we already have a UserInfo component. We can create a wrapped version by passing it to logProps:
import { logProps } from "./logProps";
import { UserInfo } from "./UserInfo";
const UserInfoWrapper = logProps(UserInfo);
export default function App() {
return (
<UserInfoWrapper
testA="Test"
testB="I am B"
testC={21}
/>
);
}logProps(UserInfo) returns a new component, so UserInfoWrapper can be used like any other React component. The three test values are logged and then forwarded to UserInfo.
Logging props is only a basic example. The same pattern can handle more useful behavior such as loading data and passing it to the wrapped component.
Fetching User Data with a HOC
Suppose several components need user data from the server. Instead of writing the request inside every component, we can create an includeUser HOC. It loads a user and gives the result to the wrapped component through a user prop.
The examples use Axios for the requests, and the application server needs to be running while testing them.
import axios from "axios";
import { useEffect, useState } from "react";
export const includeUser = (Component, userId) => {
return (props) => {
const [user, setUser] = useState(null);
useEffect(() => {
(async () => {
const response = await axios.get(`/users/${userId}`);
setUser(response.data);
})();
}, []);
return <Component {...props} user={user} />;
};
};The user starts as null because the request has not finished yet. The effect sends a GET request for the provided userId and stores the response in state. The HOC then renders the original component with its original props and one additional user prop.
A simple UserInfo component can display a loading message until the data becomes available:
export const UserInfo = ({ user }) => {
if (!user) return <h1>Loading...</h1>;
return (
<h2>
{user.name}, age: {user.age}
</h2>
);
};Create a wrapper by providing the component and the user ID:
const UserInfoWithLoader = includeUser(UserInfo, 3);
export default function App() {
return <UserInfoWithLoader />;
}UserInfo is still focused on displaying data. The generated wrapper owns the fetching behavior and supplies the result.
Creating an Updatable User HOC
Fetching a user is useful, but a form may also need to edit, reset, and save that user. We can extend the same pattern with an includeUpdatableUser HOC.
This version keeps two copies of the user:
initialUsercontains the data originally received from the server.usercontains the local data currently being edited.
Keeping the initial value allows the form to reset its fields before the changes are saved.
import axios from "axios";
import { useEffect, useState } from "react";
export const includeUpdatableUser = (Component, userId) => {
return (props) => {
const [initialUser, setInitialUser] = useState(null);
const [user, setUser] = useState(null);
useEffect(() => {
(async () => {
const response = await axios.get(`/users/${userId}`);
setInitialUser(response.data);
setUser(response.data);
})();
}, []);
const onChangeUser = (updates) => {
setUser({ ...user, ...updates });
};
const onPostUser = async () => {
const response = await axios.post(
`/users/${userId}`,
user
);
setInitialUser(response.data);
setUser(response.data);
};
const onResetUser = () => {
setUser(initialUser);
};
return (
<Component
{...props}
user={user}
onChangeUser={onChangeUser}
onPostUser={onPostUser}
onResetUser={onResetUser}
/>
);
};
};onChangeUser combines the existing user with the changed fields. onPostUser sends the edited user to the server and replaces both states with the returned data. onResetUser discards the local changes by restoring initialUser.
Building the User Form
The form does not need to know how the data is loaded or saved. It receives the user and the three functions as props from the HOC.
const UserForm = ({
user,
onChangeUser,
onPostUser,
onResetUser,
}) => {
if (!user) return <h3>Loading...</h3>;
const { name, age } = user;
return (
<form>
<label>
Name:
<input
type="text"
value={name}
onChange={(event) =>
onChangeUser({ name: event.target.value })
}
/>
</label>
<label>
Age:
<input
type="number"
value={age}
onChange={(event) =>
onChangeUser({
age: Number(event.target.value),
})
}
/>
</label>
<button onClick={onResetUser}>Reset</button>
<button onClick={onPostUser}>Save</button>
</form>
);
};The name input sends an object containing the updated name. The age input does the same for the age and uses Number to convert the input value. Reset restores the original values, while Save posts the edited user to the server.
The component can be wrapped immediately after it is created:
export const UserInfoForm = includeUpdatableUser(UserForm, 3);UserInfoForm is already wrapped, so another wrapper does not need to be created in App:
import { UserInfoForm } from "./UserInfoForm";
export default function App() {
return <UserInfoForm />;
}The HOC handles the request and state logic, while the form handles the input fields and buttons.
Making the HOC Work with Any Resource
includeUpdatableUser works, but it is limited to users. If books or other resources need the same load, edit, reset, and save behavior, repeating the complete HOC would duplicate the logic.
We can make it more flexible by receiving a resourceUrl and a resourceName. The URL identifies the server endpoint, while the name decides which props are passed to the wrapped component.
First, create a small function that capitalizes the first letter of the resource name:
const firstToCapital = (str) => {
return str.charAt(0).toUpperCase() + str.slice(1);
};If the resource name is user, the result is User. This allows the HOC to create prop names such as onChangeUser, onPostUser, and onResetUser. A book resource receives onChangeBook, onPostBook, and onResetBook instead.
The complete generic HOC looks like this:
import axios from "axios";
import { useEffect, useState } from "react";
const firstToCapital = (str) => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
export const includeUpdatableResource = (
Component,
resourceUrl,
resourceName
) => {
return (props) => {
const [initialResource, setInitialResource] = useState(null);
const [resource, setResource] = useState(null);
useEffect(() => {
(async () => {
const response = await axios.get(resourceUrl);
setInitialResource(response.data);
setResource(response.data);
})();
}, []);
const onChange = (updates) => {
setResource({ ...resource, ...updates });
};
const onPost = async () => {
const response = await axios.post(
resourceUrl,
resource
);
setInitialResource(response.data);
setResource(response.data);
};
const onReset = () => {
setResource(initialResource);
};
const capitalizedName = firstToCapital(resourceName);
const resourceProps = {
[resourceName]: resource,
[`onChange${capitalizedName}`]: onChange,
[`onPost${capitalizedName}`]: onPost,
[`onReset${capitalizedName}`]: onReset,
};
return <Component {...props} {...resourceProps} />;
};
};The state and request logic are the same as before, but none of it is hardcoded to a user. The computed property names build the correct props from resourceName.
The existing user form can now use the generic HOC:
export const UserInfoForm = includeUpdatableResource(
UserForm,
"/users/2",
"user"
);Because the resource name is user, UserForm still receives user, onChangeUser, onPostUser, and onResetUser. The same HOC can also wrap a book form by receiving a book endpoint and book as the resource name.
Why the Generated Prop Names Matter
It may look simpler to always pass props named onChange, onPost, and onReset. That works when a component receives only one resource, but it creates a conflict when higher-order components are nested.
Imagine that one component needs both a user and a book. If both wrappers provide onChange, it is not clear which function changes the user and which one changes the book. Resource-specific names solve this problem. The component can receive onChangeUser and onChangeBook without one replacing the other.
This keeps the generic HOC reusable while still making every injected prop clear.
Best Practices Shown by These HOCs
The examples follow a few connected ideas:
- Pass the incoming props to the original component with
{...props}. - Keep shared behavior inside the HOC instead of repeating it in several components.
- Allow the wrapped component to focus on displaying and editing the data it receives.
- Keep an initial resource when the component needs a Reset action.
- Receive a URL and resource name when the same behavior should work with several resources.
- Use resource-specific prop names when multiple HOCs may be nested together.
A named wrapper such as UserInfoWithLoader is clear and readable. A component can also be wrapped when it is exported, as shown with UserInfoForm. Both approaches create a new component from the original one.
Common Problems to Avoid
One common problem is forgetting to forward the original props. If the HOC returns only <Component user={user} />, any other props provided to the wrapper are lost. Passing {...props} keeps those values available to the original component.
Another problem is trying to render fetched data before it is available. The resource begins as null, so the wrapped component needs to handle that case, such as displaying a loading message before destructuring and rendering the data.
A user-specific HOC also becomes limiting when the same logic is required for books or other resources. Receiving the URL and resource name removes that restriction. However, using only generic handler names can create conflicts with nested HOCs, which is why the resource name is included in each generated prop.
Finally, the requests will not work unless the server that provides the user and resource endpoints is running. The HOC handles the client-side request, but it still needs the expected endpoint to respond.
Summary
A higher-order component is a function that receives a component and returns a new component. The generated component can inspect props, fetch data, manage editable values, and pass additional props to the original component without changing its code.
We started with logProps, then created includeUser to load a user. The updatable version added local editing, saving, and resetting. Finally, includeUpdatableResource replaced the hardcoded user details with a resource URL, resource name, and dynamically generated prop names.
The main idea is to keep reusable behavior in one place. Components can focus on their own display, while the HOC provides the extra functionality they need.