Skip to content
React rc patterns 4 min read

Container & Presentational

The container/presentational pattern splits a feature into two kinds of components: one that knows how to get and manage data, and one that only knows how to display it. Popularized by Dan Abramov in 2015, it predates hooks and was the classic way to keep data concerns out of your markup. Hooks have since blurred the line, but the underlying idea — separate logic from rendering — is still one of the most useful instincts you can have when a component starts doing too much.

The two roles

A presentational component is purely about looks. It receives everything through props, renders JSX, and stays ignorant of where the data came from or how it changes. It has no data fetching, no global state access, and ideally no local state beyond UI concerns like “is this dropdown open.”

A container component is about behavior. It fetches data, subscribes to stores, holds the state that matters, and wires up callbacks — then hands all of that to a presentational component to render.

AspectPresentationalContainer
ConcernHow things lookHow things work
Data sourceProps onlyFetch, context, store
StateUI-only, if anyOwns the real data/state
ReuseHigh — drop in anywhereLow — tied to a data source
Test styleRender with fixed propsMock data, assert behavior

A classic split

Consider a user list. The presentational piece takes an array and a loading flag and renders accordingly. It is trivial to test and to drop into Storybook.

function UserList({ users, loading, onSelect }) {
  if (loading) return <p>Loading users…</p>;
  if (users.length === 0) return <p>No users found.</p>;

  return (
    <ul className="user-list">
      {users.map((user) => (
        <li key={user.id}>
          <button onClick={() => onSelect(user.id)}>{user.name}</button>
        </li>
      ))}
    </ul>
  );
}

export default UserList;

The container owns the fetching and the state, then delegates rendering entirely to UserList.

import { useEffect, useState } from "react";
import UserList from "./UserList";

export default function UserListContainer() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let active = true;
    fetch("https://jsonplaceholder.typicode.com/users")
      .then((res) => res.json())
      .then((data) => {
        if (active) {
          setUsers(data);
          setLoading(false);
        }
      });
    return () => {
      active = false;
    };
  }, []);

  return (
    <UserList
      users={users}
      loading={loading}
      onSelect={(id) => console.log("selected", id)}
    />
  );
}

Output:

Loading users…          (initial render)
Leanne Graham           (after fetch resolves)
Ervin Howell
Clementine Bauch
selected 1              (logged when a name is clicked)

UserList can be reused anywhere data has the same shape — a search result page, an admin panel, a test — without ever touching the network.

How hooks blur the line

Before hooks, the only way to add state or lifecycle behavior was a class component, so logic naturally collected in containers. Custom hooks changed that. A hook can hold the exact data logic a container would, while leaving the component free to render — so a single component can be both “smart” and “presentable.”

import { useEffect, useState } from "react";

function useUsers() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let active = true;
    fetch("https://jsonplaceholder.typicode.com/users")
      .then((res) => res.json())
      .then((data) => active && (setUsers(data), setLoading(false)));
    return () => {
      active = false;
    };
  }, []);

  return { users, loading };
}

export default function Users() {
  const { users, loading } = useUsers();
  if (loading) return <p>Loading users…</p>;
  return (
    <ul>
      {users.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}

Here useUsers plays the container’s role and the component plays the presentational role, but they live in one file with no wrapper. For most everyday code, this is the modern default: extract logic into a hook rather than into a separate container component.

Tip: A custom hook is the spiritual successor to the container. Reach for it first. Pull out a separate presentational component only when the view itself needs to be reused or tested in isolation.

When the split still pays off

The separate-component version is not obsolete — it shines in specific situations:

  • Design systems and Storybook. A prop-only presentational component renders identically from real data or fixtures, making it ideal for visual catalogs and snapshot tests.
  • Multiple data sources, one view. If the same table must render data from a REST call on one page and from a WebSocket on another, a shared presentational component with two thin containers avoids duplicating markup.
  • Hard test seams. Testing rendering with a fixed users array needs no network mocking at all — you pass props and assert on the DOM. The data logic is tested separately against the hook or container.

For a small component that is used in exactly one place, splitting it adds files and indirection for no real gain. Let duplication or a testing pain point justify the seam.

Best practices

  • Default to a custom hook for data logic; only create a separate container component when the view is independently reusable or testable.
  • Keep presentational components free of fetching, context, and stores — pass everything through props.
  • Let presentational components hold purely UI state (open/closed, hovered) but nothing about the domain data.
  • Name the seam clearly — UserList vs UserListContainer, or useUsers for the hook — so the role is obvious at a glance.
  • Don’t split trivial, single-use components; the indirection costs more than it saves.
  • Pass callbacks down rather than letting presentational components reach back up for data they shouldn’t know about.
Last updated June 14, 2026
Was this helpful?