Skip to main content
Saved

Presentational vs. Container

Category Components

Level Beginner

Examples

React, Vue, Svelte

Separate components that handle UI rendering from those that manage data and business logic.

In this article
  1. Problem
  2. Solution
  3. Example
  4. Presentational and Container Pattern
  5. Modern Hooks Alternative
  6. Reusable Presentational Component
  7. Testing Presentational Components
  8. Benefits
  9. Tradeoffs
  10. Summary
  11. See also

Presentational vs. Container

Problem

You open a component file to fix a CSS bug and find yourself staring at 800 lines of API calls, error handling, data transformation, and JSX all mixed together, with no clear boundary between styling logic and business logic.

I’ve reviewed components where changing a button’s color required understanding the entire data fetching pipeline. Designers couldn’t iterate on UI without running a backend, and testing meant mocking every API the component called.

It gets worse when you need to reuse the UI. You built a nice UserList component, but it’s hardwired to one specific API. Now marketing wants the same list design with data from a different endpoint, and your options are copying the component and maintaining both versions, or a full refactor.

Solution

The pattern splits components into two types: presentational components that only render UI, and container components that fetch data and manage state.

Presentational components are dumb on purpose. They receive data through props and render it, with no fetching, side effects, or business logic, so testing them means passing props and checking the output.

Container components do the rest: fetching data, managing state, handling errors, and passing everything down to presentational components. That complexity stays in the container, separate from the rendering logic.

Once UserList only handles presentation, you can feed it data from anywhere: a REST API, GraphQL, local storage, or a WebSocket. The UI stays the same whatever the data source, and its tests don’t need mocks.

Hooks have largely replaced this pattern in modern React, but separating “what it looks like” from “how it works” is still useful.

Example

Each example has a presentational component that only renders and a container that handles the data work.

Presentational and Container Pattern

// Presentational - only renders UI
function UserList({ users, onUserClick, loading, error }) {
  if (loading) return <div className="spinner">Loading...</div>;
  if (error) return <div className="error">{error.message}</div>;
  if (users.length === 0) return <div className="empty">No users</div>;

  return (
    <ul className="user-list">
      {users.map(user => (
        <li key={user.id} onClick={() => onUserClick(user.id)}>
          <img src={user.avatar} alt="" />
          <h3>{user.name}</h3>
        </li>
      ))}
    </ul>
  );
}

// Container - handles data fetching and state
function UserListContainer() {
  const navigate = useNavigate();
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchUsers()
      .then(setUsers)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);

  const handleUserClick = async (userId) => {
    await trackUserClick(userId);
    navigate(`/user/${userId}`);
  };

  return (
    <UserList
      users={users}
      onUserClick={handleUserClick}
      loading={loading}
      error={error}
    />
  );
}

Modern Hooks Alternative

Custom hooks can replace container components by extracting data logic while keeping everything in one file:

// Custom hook extracts data logic - replaces container component
function useUsers() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchUsers()
      .then(setUsers)
      .catch(setError)
      .finally(() => setLoading(false));
  }, []);

  return { users, loading, error };
}

// Component uses hook instead of container wrapper
function UsersPage() {
  const navigate = useNavigate();
  const { users, loading, error } = useUsers();
  if (loading) return <Spinner />;
  if (error) return <Error error={error} />;
  return <UserList users={users} onUserClick={(id) => navigate(`/user/${id}`)} />;
}

Reusable Presentational Component

The same presentational component works with different data sources since it only cares about props:

// Same presentational component, different data sources
function ProductList({ items, onItemClick }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onItemClick(item.id)}>{item.name}</li>
      ))}
    </ul>
  );
}

// From API
function APIProducts() {
  const [items, setItems] = useState([]);
  useEffect(() => { fetchProducts().then(setItems); }, []);
  return <ProductList items={items} onItemClick={trackClick} />;
}

// From WebSocket
function LiveProducts() {
  const [items, setItems] = useState([]);
  useEffect(() => {
    const ws = new WebSocket('/products');
    ws.onmessage = (e) => setItems(JSON.parse(e.data));
    return () => ws.close();
  }, []);
  return <ProductList items={items} onItemClick={trackClick} />;
}

Testing Presentational Components

Testing becomes trivial when components just render props with no mocking required:

test('renders user list', () => {
  const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
  const { getByText } = render(<UserList users={users} />);
  expect(getByText('Alice')).toBeInTheDocument();
  expect(getByText('Bob')).toBeInTheDocument();
});

test('calls onClick handler', () => {
  const handleClick = jest.fn();
  const { getByText } = render(
    <UserList users={[{ id: 1, name: 'Alice' }]} onUserClick={handleClick} />
  );
  fireEvent.click(getByText('Alice'));
  expect(handleClick).toHaveBeenCalledWith(1);
});

Benefits

  • Presentational components work with any data source (REST, GraphQL, WebSocket, cache) because nothing in them depends on where the data comes from.
  • Testing is simple: you pass props and check the output, with no mocking. Storybook documentation becomes easy with different prop combinations.
  • Designers can iterate on UI without understanding data fetching, and API changes only affect the container, leaving presentational components untouched.
  • Components become portable across projects; that UserList works anywhere that passes it the right props.

Tradeoffs

  • More files and more indirection: every feature needs at least two components, and you’re constantly jumping between them.
  • Overkill for simple components; a button used in one place doesn’t need this separation.
  • Hooks have largely replaced this pattern in modern React, extracting data logic without wrapper components.
  • Props drilling gets tedious when containers pass fifteen props down, and boundaries get fuzzy (does date formatting belong in presentation or container?).
  • Modern thinking favors colocation, keeping related code together rather than splitting by type.

Summary

Separating presentational components from containers divides your UI into dumb components that render props and smart components that manage data. The presentational layer becomes reusable and trivial to test, while containers handle coordination. Modern hooks have largely replaced explicit containers, but separating what things look like from how they work is still useful.

See also

Newsletter

A Monthly Email
from Den Odell

Behind-the-scenes thinking on frontend patterns, site updates, and more

No spam. Unsubscribe anytime.