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 pattern divides components into two types:
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:
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}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}Separating logic from view provides several key advantages that are invaluable in large projects:
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});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.A designer can work on the presentational component (appearance, animations), while a developer works on the hook (logic, API) -- simultaneously and without conflicts.
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.