We use cookies to enhance your experience on the site
CodeWorlds

Container/Presentational Pattern -- Separating logic from view

In the Space Station, every system has two layers: control logic (command center) and display (information panels). In React, we apply an analogous separation -- the Container/Presentational pattern.

The idea behind the pattern

The pattern divides components into two types:

  • Container (Smart Component) -- manages state, fetches data, handles business logic. Contains no JSX that displays UI.
  • Presentational (Dumb Component) -- is solely responsible for rendering UI. Receives data through props, doesn't know where they come from.

Presentational Component

A presentational component is simple. It receives data and displays it:

1// CrewList.jsx -- presentational component
2function CrewList({ members, onSelect, selectedId }) {
3  return (
4    <ul className="crew-list">
5      {members.map(member => (
6        <li
7          key={member.id}
8          className={member.id === selectedId ? 'active' : ''}
9          onClick={() => onSelect(member.id)}
10        >
11          <span className="name">{member.name}</span>
12          <span className="role">{member.role}</span>
13        </li>
14      ))}
15    </ul>
16  );
17}

Characteristics of a presentational component:

  • Has no own state (or only UI state like hover)
  • Receives data and callbacks through props
  • Easy to test -- just pass props
  • Reusable in different contexts

Container Component

The container manages state and logic:

1// CrewContainer.jsx -- container component
2function CrewContainer() {
3  const [members, setMembers] = useState([]);
4  const [selectedId, setSelectedId] = useState(null);
5  const [loading, setLoading] = useState(true);
6
7  useEffect(() => {
8    fetch('/api/crew')
9      .then(res => res.json())
10      .then(data => {
11        setMembers(data);
12        setLoading(false);
13      });
14  }, []);
15
16  const handleSelect = (id) => {
17    setSelectedId(id);
18  };
19
20  if (loading) return <Spinner />;
21
22  return (
23    <CrewList
24      members={members}
25      onSelect={handleSelect}
26      selectedId={selectedId}
27    />
28  );
29}

Evolution of the pattern -- Custom Hooks

Nowadays, instead of container components, we often use custom hooks:

1// useCrew.js -- hook instead of container component
2function useCrew() {
3  const [members, setMembers] = useState([]);
4  const [selectedId, setSelectedId] = useState(null);
5  const [loading, setLoading] = useState(true);
6
7  useEffect(() => {
8    fetch('/api/crew')
9      .then(res => res.json())
10      .then(data => {
11        setMembers(data);
12        setLoading(false);
13      });
14  }, []);
15
16  const selectMember = (id) => setSelectedId(id);
17
18  return { members, selectedId, selectMember, loading };
19}
20
21// Usage -- logic in hook, UI in component
22function CrewPage() {
23  const { members, selectedId, selectMember, loading } = useCrew();
24
25  if (loading) return <Spinner />;
26
27  return <CrewList members={members} onSelect={selectMember} selectedId={selectedId} />;
28}

Benefits of separation

Separating logic from view provides several key advantages that are invaluable in large projects:

1. Easy testing

Presentational components can be tested without mocking APIs:

1// Testing presentational component -- zero mocks!
2test('renders crew list', () => {
3  const members = [
4    { id: 1, name: 'Anna', role: 'Pilot' },
5    { id: 2, name: 'Jan', role: 'Engineer' },
6  ];
7
8  render(<CrewList members={members} onSelect={() => {}} selectedId={null} />);
9
10  expect(screen.getByText('Anna')).toBeInTheDocument();
11  expect(screen.getByText('Engineer')).toBeInTheDocument();
12});
13
14// Testing hook -- we test logic separately
15test('useCrew fetches and returns members', async () => {
16  const { result } = renderHook(() => useCrew());
17  await waitFor(() => expect(result.current.members).toHaveLength(5));
18});

2. Reusability

The

CrewList
component can be used in different contexts -- on a dashboard, in a modal, in a side panel -- each time with a different data source.

3. Independent development

A designer can work on the presentational component (appearance, animations), while a developer works on the hook (logic, API) -- simultaneously and without conflicts.

When to use this pattern?

  • Always when a component fetches data from an API and displays it
  • Always when the same logic is used in multiple places
  • Not for simple components with minimal logic -- over-abstraction is also a problem

This pattern is the foundation of good React architecture. Separating "what to display" from "where to get the data" makes the code more predictable, testable, and flexible -- like separate navigation and display systems on a spaceship.

Go to CodeWorlds