JavaScript and React course Β· Module 15: Patterns and Architecture
Container/Presentational Pattern - Separating logic from view
In this lesson6
The crew list on the bridge fetches data from an API, remembers the selected person, shows a spinner and draws the cards, all in a single component. When Commander Nova asks for the same list in a modal window, but with a different data source, you have to copy the whole component along with the fetching. 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. It has no markup or styles of its own, it only renders presentational components and passes data to them.
- Presentational (Dumb Component): is solely responsible for rendering UI. It receives data through props and does not know where it comes from.
Dan Abramov popularized the pattern in 2015. In 2019 he added a note to his article saying he no longer recommends forcing components into this split, because hooks let you separate logic without an artificial division. The idea itself remains one of the most important in React, though, so let's look at both layers.
Presentational Component
We start with the display. A presentational component is simple: it receives data and a function for handling clicks, and it fetches nothing itself:
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}CrewList does not know whether the data came from an API, a test or localStorage. It does not change the selection on its own either: it just calls onSelect and leaves the decision to its parent.
Characteristics of a presentational component:
- Has no state of its own (or only UI state, like hover)
- Receives data and callbacks through props
- Easy to test: just pass props
- Reusable in different contexts
Container Component
Now the command center. The container manages state and logic: it holds the crew list, the loading flag and the id of the selected person, and it fetches the data after mounting:
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 }, []);The second half of the container handles picking a crew member and passes everything down through props to the presentational component:
1 const handleSelect = (id) => {
2 setSelectedId(id);
3 };
4
5 if (loading) return <Spinner />;
6
7 return (
8 <CrewList
9 members={members}
10 onSelect={handleSelect}
11 selectedId={selectedId}
12 />
13 );
14}Notice what is missing here: not a single CSS class, not a single <li>. The container renders only Spinner and CrewList. When a designer rebuilds the list into a grid of cards, the container will not change.
Evolution of the pattern - Custom Hooks
Nowadays, instead of container components, we more often write custom hooks. A custom hook is a function whose name starts with use and which calls other hooks. We move all of the container's logic into it:
1// useCrew.js: a hook instead of a 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}The hook returns an object with data and actions, but it does not return JSX. That is why the component using it stays short and readable:
1// Usage: logic in the hook, UI in the component
2function CrewPage() {
3 const { members, selectedId, selectMember, loading } = useCrew();
4
5 if (loading) return <Spinner />;
6
7 return <CrewList members={members} onSelect={selectMember} selectedId={selectedId} />;
8}The logic is the same as in CrewContainer, only its location changed: instead of a separate component we have a function you can call in any component. A hook can also return derived values, for example a selectedMember found with members.find(). The React docs advise calculating such data during rendering instead of keeping it in separate state, because then it cannot drift out of sync with the original. The layers stack from the bottom: data from the API, the hook with logic and state, the container component as the connector (here CrewPage), the presentational component and the rendered UI.
Benefits of separation
Separating logic from view provides several advantages that are invaluable in large projects.
1. Easy testing
Presentational components can be tested without mocking APIs. You just give them props and check what appeared on the screen:
1// Testing a 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});You test the logic separately. renderHook from React Testing Library runs the hook without any view, and waitFor waits until the data shows up:
1// Testing the hook: we test the logic separately
2test('useCrew fetches and returns members', async () => {
3 const { result } = renderHook(() => useCrew());
4 await waitFor(() => expect(result.current.members).toHaveLength(5));
5});This second test still needs a substituted fetch, for example through jest.fn() or the MSW library, because the hook really fetches data. Here the mocked API returns five people. Only the presentational component test gets by without mocks.
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?
- Yes, when a component fetches data from an API and displays it: the logic goes to a hook, the view to a presentational component
- Yes, when the same logic is used in multiple places
- No for simple components with minimal logic: over-abstraction is also a problem
My advice: do not create a CrewContainer.jsx file out of habit. Start with one component, and when the logic grows or a second screen needs the same data, extract it into a hook. The idea behind this pattern is the foundation of good React architecture. Separating "what to display" from "where to get the data" makes code more predictable, testable and flexible, like separate navigation and display systems on a spaceship. In the next lesson we will go further and build headless components that hand the whole appearance over to the consumer.
Remember: the command center gets the data, and the panel only shows it.
Code for this lesson: App.jsx
1import React, { useState, useEffect } from 'react';
2
3// The data keeps the role values (they also build the role-... CSS classes), the screen shows readable labels
4const ROLE_LABELS = { captain: 'Captain', medic: 'Medic', engineer: 'Engineer', navigator: 'Navigator', scientist: 'Scientist' };
5
6// === PRESENTATIONAL COMPONENT ===
7// Pure display, no data fetching, no business logic
8function CrewList({ members, selectedId, onSelect }) {
9 return (
10 <div className="crew-list">
11 {members.map(member => (
12 <div
13 key={member.id}
14 className={`crew-card ${member.id === selectedId ? 'selected' : ''}`}
15 onClick={() => onSelect(member.id)}
16 >
17 <div className="avatar">{member.name[0]}</div>
18 <div className="info">
19 <h4>{member.name}</h4>
20 <span className={`role role-${member.role}`}>{ROLE_LABELS[member.role]}</span>
21 </div>
22 <span className={`status ${member.active ? 'active' : 'inactive'}`}>
23 {member.active ? 'Active' : 'Off-duty'}
24 </span>
25 </div>
26 ))}
27 </div>
28 );
29}
30
31// === PRESENTATIONAL: Detail view ===
32function CrewDetail({ member }) {
33 if (!member) return <div className="detail-empty">Select a crew member</div>;
34
35 return (
36 <div className="crew-detail">
37 <div className="detail-avatar">{member.name[0]}</div>
38 <h2>{member.name}</h2>
39 <p className="detail-role">{ROLE_LABELS[member.role]}</p>
40 <div className="stats">
41 <div className="stat"><span>Missions</span><strong>{member.missions}</strong></div>
42 <div className="stat"><span>Experience</span><strong>{member.exp} years</strong></div>
43 <div className="stat"><span>Rating</span><strong>{member.rating}/5</strong></div>
44 </div>
45 </div>
46 );
47}
48
49// === CUSTOM HOOK (modern replacement for container) ===
50function useCrew() {
51 const [members, setMembers] = useState([]);
52 const [selectedId, setSelectedId] = useState(null);
53 const [loading, setLoading] = useState(true);
54
55 useEffect(() => {
56 // Simulate API fetch
57 const timer = setTimeout(() => {
58 setMembers([
59 { id: 1, name: 'Commander Nova', role: 'captain', active: true, missions: 42, exp: 15, rating: 5 },
60 { id: 2, name: 'Dr. Stellar', role: 'medic', active: true, missions: 28, exp: 8, rating: 4 },
61 { id: 3, name: 'Astro', role: 'engineer', active: false, missions: 35, exp: 12, rating: 5 },
62 { id: 4, name: 'Ra', role: 'navigator', active: true, missions: 50, exp: 20, rating: 5 },
63 { id: 5, name: 'Nebula', role: 'scientist', active: true, missions: 19, exp: 5, rating: 4 },
64 ]);
65 setLoading(false);
66 }, 800);
67 // Cleanup: if the component unmounts early, cancel the timer
68 return () => clearTimeout(timer);
69 }, []);
70
71 const selectedMember = members.find(m => m.id === selectedId) || null;
72
73 return { members, selectedId, setSelectedId, selectedMember, loading };
74}
75
76// === PAGE COMPONENT ===
77export default function App() {
78 const { members, selectedId, setSelectedId, selectedMember, loading } = useCrew();
79
80 if (loading) {
81 return <div className="loading"><div className="spinner" /><p>Loading crew data...</p></div>;
82 }
83
84 return (
85 <div className="app">
86 <h1>Container / Presentational Pattern</h1>
87 <p className="subtitle">Logic in hook, UI in components</p>
88 <div className="layout">
89 <div className="list-panel">
90 <h3>Crew ({members.length})</h3>
91 <CrewList members={members} selectedId={selectedId} onSelect={setSelectedId} />
92 </div>
93 <div className="detail-panel">
94 <CrewDetail member={selectedMember} />
95 </div>
96 </div>
97 </div>
98 );
99}Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is the purpose of the 'children' prop in the context of the composition pattern?
2. Which characteristic is NOT typical of a Presentational component?
Hands-on tasks in the game
- Code editor
Finish two components built with composition. SpaceCard is a container (containment). ___BLANK1___: the border colour is the value from the VARIANT_COLORS object for the variant prop (info, warning or danger). ___BLANK2___: the card body shows everything the parent writes between <SpaceCard> and </SpaceCard>. SpaceLayout has three slots: ___BLANK3___ shows the element from the header prop in <header>, ___BLANK4___ shows the element from the sidebar prop in <aside>, and the children go into <main>.
- Click in order
Arrange the code of the specialised LaunchButton component that renders SpaceButton with preset props:
- Code editor
Separate the logic from the view. The usePlanets hook gets a loadPlanets function (by default the simulated API fetchPlanets) and loads the data in useEffect. ___BLANK1___: store the received data in the planets state. ___BLANK2___: selectedPlanet is the planet whose id equals selectedId, computed during rendering from the planets array (null when nothing is selected). PlanetList is a presentational component: ___BLANK3___ calls onSelect with the id of that planet when its button is clicked. ___BLANK4___: App passes PlanetList the function from the hook that changes the selection.