Design Patterns

Container Component React Design Pattern

I used to build React components that fetched data, managed loading states, handled errors, and rendered UI — all in one file. Every component was 200+ li...

14 Apr 2024

Container Component React Design Pattern

I used to build React components that fetched data, managed loading states, handled errors, and rendered UI — all in one file. Every component was 200+ lines. Testing meant mocking APIs just to verify that a button had the right label.

The Container Component pattern splits that mess into two kinds of components:

Container components handle the logic. Data fetching, state management, side effects. They know what data exists but don't care how it looks on screen.

Presentational components handle the UI. They receive data via props and render it. They don't know where the data comes from.

Jsx
// Container Component
import React, { useState, useEffect } from 'react';
import UserList from './UserList';

const UserContainer = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => {
        setUsers(data);
        setLoading(false);
      })
      .catch(err => console.error('Error fetching users:', err));
  }, []);

  if (loading) return <p>Loading...</p>;

  return <UserList users={users} />;
};

export default UserContainer;
Jsx
// Presentational Component
import React from 'react';

const UserList = ({ users }) => (
  <ul>
    {users.map(user => (
      <li key={user.id}>{user.name}</li>
    ))}
  </ul>
);

export default UserList;

UserContainer owns the data. UserList owns the markup. You can test UserList with a hardcoded array of users — no API mocking needed. You can reuse UserList anywhere you have user data, regardless of where it came from.

Why This Matters

  • Testability. Presentational components are pure functions of their props. Unit testing is trivial.
  • Reusability. The same UserList works whether the data comes from REST, GraphQL, or a mock.
  • Readability. Each component does one thing. You know exactly where to look when something breaks.

The benefit: Clean separation of concerns. Smaller files. Components that are easy to test, easy to reuse, and easy to reason about.

The cost: More files. For tiny features, splitting into container and presentational components can feel like overhead. Also, with hooks like useQuery from React Query or SWR, the container layer often becomes unnecessary — the hook is the container logic. This pattern was more critical in the class component era.

I still use this split for complex components where the data-fetching logic is substantial. But for simple cases, a single component with a custom hook achieves the same separation without the extra file.

Keep reading