Container Components: One Place for Data, Many Places to Display It
Every component that fetches its own data creates a duplication problem. Container components fix this by extracting data loading into a wrapper that passes results down automatically.
The design patterns overview set up the problem. Now here is the first one that actually changes how you structure a real app.
I used to think the right place for a useEffect data fetch was inside the component that displays the data. That felt natural. UserProfile fetches user data. BookCard fetches book data. Each component is self-contained.
Then a second component needed the same user data.
And there was no clean way to share it.
Duplicating the useEffect in both components is wrong. Lifting the data to a parent and passing it as props works, but now the parent knows too much. What you actually want is a component whose only job is loading data and handing it to whatever child needs it.
That is a container component.
What It Does
A container component fetches data. It then clones its children and injects the fetched data as a prop onto each one. The children never call useEffect. They just receive props and render.
const CurrentUserLoader = ({ children }) => {
const [user, setUser] = useState(null);
useEffect(() => {
(async () => {
const response = await axios.get('/current-user');
setUser(response.data);
})();
}, []);
return (
<>
{React.Children.map(children, child => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { user });
}
return child;
})}
</>
);
};React.Children.map iterates over the children. React.cloneElement clones each valid element and attaches user as an additional prop.
The child component receives user without knowing where it came from:
const UserProfile = ({ user }) => (
<div>
<h2>{user?.name}</h2>
<p>{user?.email}</p>
</div>
);
<CurrentUserLoader>
<UserProfile />
</CurrentUserLoader>Why This Works as a Layout Component Parallel
The layout components chapter established that content components should not know where they render. This pattern applies the same idea to data: content components should not know where their data comes from.
UserProfile is just a display component. It accepts a user prop and renders it. Whether that prop came from a container, a parent, a Redux store, or a test fixture does not matter. The component stays the same.
A Note on React.cloneElement
cloneElement injects props silently. Someone reading <UserProfile /> in the JSX does not immediately see where user comes from. That can be confusing.
Use it deliberately, and only when the container pattern is the explicit design. In a container component, it is appropriate because the whole point is that the container owns the data and the children display it. The structure makes the intent clear.
For everything else, pass props explicitly.
The next post takes this container and generalizes it until it works with any URL, any data type, and any source, not just the current user endpoint.
The Essentials
- A container component fetches data and passes it down. Display components receive data and render it. They should never mix.
React.Children.map+React.cloneElementlets the container inject props onto any valid child element.- Display components stay identical whether their data comes from a container, a parent, or a test fixture.
- Use
cloneElementdeliberately, in explicit container patterns. Do not use it as a general prop-shortcut.
Further Reading and Watching
- React Hooks: What Will Happen to the Container/Presenter Pattern? by Kent C. Dodds. Essential context on how this pattern evolved with hooks.
- React docs: cloneElement includes a warning about when to use it and the alternatives. Worth reading before using it in production.
Practice what you just read.
Keep reading