Back to all notes
Presentational vs Container Components: Separate How It Looks from How It Works
Extract behavior into hooks, render with pure components, and keep your features composable.
Introduction
Presentational components worry about how things look; container components worry about how things work. This pattern keeps UI markup clean while isolating state and business logic.
Why this matters
Mixing state management with rendering logic makes components hard to reuse and test. The presentational/container pattern fixes that:
- Presentational components render UI from props.
- Container components manage state and pass props down.
The problem
Mixing concerns—storing todos and rendering the list inline—bloats components. Any new behavior (filters, persistence) grows the same file.
Inefficient approach
UI and behavior are tangled—duplication grows fast:
import { useState } from "react";
function TodoContainer() {
const [todos, setTodos] = useState([
{ id: 1, text: "Learn React", completed: false },
{ id: 2, text: "Practice design patterns", completed: true },
{ id: 3, text: "Build awesome apps", completed: false },
]);
const toggleTodo = (id) => {
setTodos(todos.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
));
};
return (
<ul>
{todos.map((todo) => (
<li
key={todo.id}
style={{ textDecoration: todo.completed ? "line-through" : "none" }}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</li>
))}
</ul>
);
}
function App() {
return (
<div>
<h1>Presentational vs Container Components</h1>
<TodoContainer />
</div>
);
}
export default App;
The solution
Extract behavior into a hook and render with a pure component.
import { useState } from "react";
function useTodos() {
const [todos, setTodos] = useState([
{ id: 1, text: "Learn React", completed: false },
{ id: 2, text: "Practice design patterns", completed: true },
{ id: 3, text: "Build awesome apps", completed: false },
]);
const toggleTodo = (id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
return { todos, toggleTodo };
}
function TodoList({ todos, onToggle }) {
return (
<ul>
{todos.map((todo) => (
<li
key={todo.id}
style={{
textDecoration: todo.completed ? "line-through" : "none",
cursor: "pointer",
}}
onClick={() => onToggle(todo.id)}
>
{todo.text}
</li>
))}
</ul>
);
}
function TodoContainer() {
const { todos, toggleTodo } = useTodos();
return (
<div>
<h2>Todo List</h2>
<TodoList todos={todos} onToggle={toggleTodo} />
</div>
);
}
function App() {
return (
<div>
<h1>Presentational vs Container Components</h1>
<TodoContainer />
</div>
);
}
export default App;
Step-by-step
- Move todo state and mutations into
useTodos. - Create
TodoListthat acceptstodosandonTogglevia props. - Replace inline rendering with
<TodoList />in a container. - Keep container responsible for data; keep list responsible for UI.
- Confirm toggling works and UI remains stateless.
Why this is better
- Presentational components are easy to reuse in different containers.
- Hooks centralize behavior and keep containers lean.
- You can test UI and behavior independently.
Practical tips
- Keep presentational components pure—no side effects.
- Keep container props minimal. Pass functions and the exact data required.
- If your UI needs slight variations, prefer small props (e.g.,
variant) over branching inside the component.
Knowledge base
Problem snippet:
setTodos(todos.map(/* inline mutation logic */))
<ul>{todos.map(/* inline render */)}</ul>
Solution snippet:
function useTodos() { /* state + toggle */ }
<TodoList todos={todos} onToggle={toggleTodo} />