React Design Patterns: Container Components
Sep 9, 2026
React Design Patterns: Container Components
Container components are React components responsible for loading and managing data for their child components. The container handles the data source and passes the result down, while the child focuses on displaying the data.
This separation is useful when several components need similar data-loading logic. Instead of repeating useState, useEffect, and a request in every child, the logic can be moved into a container component and reused.
The main idea is that a child component should not need to know where its data comes from. It should receive props and display the relevant content.
What Are Container Components?
A child component can load its own data with hooks such as useState and useEffect, using a request library such as Axios. This works for a small example, but it becomes repetitive when several components need similar logic.
A container component solves this by doing the following:
- Loading data from a source.
- Keeping the loaded value in state.
- Passing the value to child components.
The child component then only deals with presentation. For example, a UserInfo component can receive a user prop and display the user's name and age without knowing whether the data came from a server or another source.
Preparing the Example
The examples use a small server with user and book data. The React application requests that data through endpoints. Axios is used for fetching data, and Express can be used to create the simple server.
Install the packages used in the example:
npm install express axiosThe server contains users, books, a current user, and endpoints for retrieving them. The server runs on its own port, while the React application runs separately. Both the server and the React application need to be running when testing the examples.
A Simple Current User Loader
Start with a container that loads the current user. The data is initially null because no user has been loaded yet.
import { useEffect, useState } from "react";
import axios from "axios";
export const CurrentUserLoader = ({ children }) => {
const [user, setUser] = useState(null);
useEffect(() => {
const loadUser = async () => {
const response = await axios.get("/current-user");
setUser(response.data);
};
loadUser();
}, []);
return children;
};The request runs once when the container is first rendered. The response data is saved in the user state. The next step is passing that state to the child component.
Passing Data to Children
The child can be placed inside the loader:
<CurrentUserLoader>
<UserInfo />
</CurrentUserLoader>The child is available through the children prop. React provides utilities that allow the container to inspect the children and attach extra props to valid React elements.
import React from "react";
import { useEffect, useState } from "react";
import axios from "axios";
export const CurrentUserLoader = ({ children }) => {
const [user, setUser] = useState(null);
useEffect(() => {
const loadUser = async () => {
const response = await axios.get("/current-user");
setUser(response.data);
};
loadUser();
}, []);
return (
<>
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { user });
}
return child;
})}
</>
);
};React.Children.map loops through the children. React.isValidElement checks whether the current child is a React element. If it is, React.cloneElement returns the child with an additional user prop.
The UserInfo component can now focus only on displaying the value:
export const UserInfo = ({ user }) => {
const { name, age } = user || {};
return (
<h2>
{name}, age: {age}
</h2>
);
};The loader owns the data-fetching logic, while UserInfo owns the display logic.
Making the Loader More Generic
CurrentUserLoader is useful, but it can only fetch one particular resource. A more flexible container can receive a user ID and load the matching user.
import React from "react";
import { useEffect, useState } from "react";
import axios from "axios";
export const UserLoader = ({ userId, children }) => {
const [user, setUser] = useState(null);
useEffect(() => {
const loadUser = async () => {
const response = await axios.get(`/users/${userId}`);
setUser(response.data);
};
loadUser();
}, [userId]);
return (
<>
{React.Children.map(children, (child) =>
React.isValidElement(child)
? React.cloneElement(child, { user })
: child
)}
</>
);
};The userId is part of the request URL and the effect dependency list. When the ID changes, the container fetches the data for the new user.
It can be used like this:
<UserLoader userId={3}>
<UserInfo />
</UserLoader>Several loaders can be used with different IDs. The child component remains the same because the container provides the requested user through the same prop.
Creating a Resource Loader
The same idea can be made more generic again. Instead of creating a component that only loads users, create a ResourceLoader that receives a URL and the name of the prop that should be passed to the child.
import React from "react";
import { useEffect, useState } from "react";
import axios from "axios";
export const ResourceLoader = ({
resourceUrl,
resourceName,
children,
}) => {
const [resource, setResource] = useState(null);
useEffect(() => {
const loadResource = async () => {
const response = await axios.get(resourceUrl);
setResource(response.data);
};
loadResource();
}, [resourceUrl]);
return (
<>
{React.Children.map(children, (child) =>
React.isValidElement(child)
? React.cloneElement(child, {
[resourceName]: resource,
})
: child
)}
</>
);
};The resourceUrl identifies what should be loaded. The resourceName identifies the prop name that the child expects. This allows the same container to load users and books.
For a user, the child can receive a user prop:
<ResourceLoader
resourceUrl="/users/2"
resourceName="user"
>
<UserInfo />
</ResourceLoader>For a book, the same container can provide a book prop to a different child:
<ResourceLoader
resourceUrl="/books/1"
resourceName="book"
>
<BookInfo />
</ResourceLoader>The data-loading state and effect are not repeated in separate user and book containers. Only the URL, prop name, and child component change.
Separating the Data Source
The ResourceLoader is more reusable, but it still knows that Axios is being used and that the data comes from a URL. We can separate this responsibility further by passing a function that retrieves the data.
The new container can be called DataSource. It receives a getData function instead of a resource URL.
import React from "react";
import { useEffect, useState } from "react";
export const DataSource = ({
getData = () => {},
resourceName,
children,
}) => {
const [resource, setResource] = useState(null);
useEffect(() => {
const loadResource = async () => {
const data = await getData();
setResource(data);
};
loadResource();
}, [getData]);
return (
<>
{React.Children.map(children, (child) =>
React.isValidElement(child)
? React.cloneElement(child, {
[resourceName]: resource,
})
: child
)}
</>
);
};Now the data source is outside the container. The function can use Axios to load a user from the server:
import axios from "axios";
const getDataFromServer = async (url) => {
const response = await axios.get(url);
return response.data;
};
<DataSource
getData={() => getDataFromServer("/users/2")}
resourceName="user"
>
<UserInfo />
</DataSource>;The DataSource component does not need to know that the data came from Axios or from a server. It only calls getData, saves the returned value, and passes it to the child.
Loading Data from Local Storage
Because the container receives a function, the data does not have to come from a server. A separate function can read a value from browser local storage.
const getDataFromLocalStorage = (key) => {
return localStorage.getItem(key);
};Create a small component that receives the data through a prop:
const Message = ({ msg }) => {
return <h1>{msg}</h1>;
};The same DataSource can now provide a value from local storage:
<DataSource
getData={() => getDataFromLocalStorage("test")}
resourceName="msg"
>
<Message />
</DataSource>The child does not change. Only the getData function changes, which shows why separating the data source from the container makes the pattern more flexible.
Using a Render Prop Instead
Passing data by cloning children is one way to implement the container pattern. Another option is the render prop, also called a render function.
Instead of passing the child directly, pass a function called render. That function receives the resource and returns the component that should be rendered.
<DataSourceWithRender
getData={() => getDataFromServer("/users/2")}
render={(resource) => <UserInfo user={resource} />}
/>The container can call the render function after loading the data:
import { useEffect, useState } from "react";
export const DataSourceWithRender = ({ getData, render }) => {
const [resource, setResource] = useState(null);
useEffect(() => {
const loadResource = async () => {
const data = await getData();
setResource(data);
};
loadResource();
}, [getData]);
return render(resource);
};The render function receives the loaded resource and decides which component should receive it. This produces the same result as cloning the child, but the data flow is written directly in the render function.
cloneElement and Maintainability
React.cloneElement and React.Children are useful in a container component when the purpose of the container is clear: collect data and pass it to its children.
They should not be used everywhere for ordinary prop passing. When a component silently adds props to another component, someone using it may find it difficult to understand where those props came from. That can make the code less maintainable and cause confusion for other developers.
In this pattern, the data flow is easier to understand because the container has a specific responsibility. The data-loading logic is isolated in one place, and the child is used to display the resulting data.
If the cloned-child approach does not fit the way the code is organized, the render prop provides an alternative. The choice depends on the structure of the code and the way the data needs to be passed.
Summary
Container components handle data loading and management for child components. A current-user loader can be made more flexible by accepting a user ID, and a resource loader can be made even more reusable by accepting a URL and a prop name.
The DataSource pattern separates the data source from the container completely. It receives a getData function, so the same component can load data from a server, local storage, or another source. The data can be passed to children with React.cloneElement or supplied through a render prop.
The central principle is to keep data management separate from data display. The container manages how data is obtained, while the child receives props and focuses on showing the content.