Toggling “Completed”
Click a checkbox; the todo flips between completed and not. The pattern is the same one you’ll use for editing in step 9: produce a new object with one field changed, and a new array with that one item replaced.
Goal
Clicking a todo’s checkbox toggles its completed field. The UI re-renders to show the new checked state.
React concepts you’ll use
- Controlled checkboxes (same shape as step 6’s text input)
- Object spread for immutable updates — see Destructuring and spread
.mapto replace one item in an array- Handlers down, data up
Implementation
-
TodoListexposes a handler that takes an updated todo and replaces the matching item in state immutably - That handler is passed to each
<Todo />as a prop - The checkbox in
Todohas a realonChange(dropdisabledandreadOnly) - When the checkbox changes,
Todobuilds a new todo object withcompletedflipped and asks the parent to apply it - The parent’s array is updated to a new array (not mutated)
Try it
Make the checkboxes interactive. Clicking a todo’s checkbox flips its completion state.
Solution
// src/components/Todo.jsx
export default function Todo({ todo, handleUpdateTodo }) {
const handleCheckboxClick = () =>
handleUpdateTodo({ ...todo, completed: !todo.completed });
return (
<li>
<label htmlFor={todo.id}>
<input
type="checkbox"
id={todo.id}
checked={todo.completed}
onChange={handleCheckboxClick}
/>
<span>{todo.label}</span>
</label>
</li>
);
}// src/components/TodoList.jsx
import { useState } from 'react';
import Todo from './Todo';
import TodoComposer from './TodoComposer';
export default function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, label: 'Learn React', completed: false },
{ id: 2, label: 'Learn Next.js', completed: false },
{ id: 3, label: 'Learn React Query', completed: false },
]);
const handleAddTodo = (newTodo) => {
setTodos([...todos, newTodo]);
};
const handleUpdateTodo = (updatedTodo) => {
setTodos(
todos.map((todo) => (todo.id === updatedTodo.id ? updatedTodo : todo)),
);
};
return (
<ul>
<TodoComposer handleAddTodo={handleAddTodo} />
{todos.map((todo) => (
<Todo
key={todo.id}
todo={todo}
handleUpdateTodo={handleUpdateTodo}
/>
))}
</ul>
);
}Why this works
Two immutable patterns stack: { ...todo, completed: !todo.completed } makes a new object with one field changed, and todos.map(t => t.id === updated.id ? updated : t) produces a new array with that item replaced and every other reference preserved. Identity preservation is what lets React skip re-rendering unchanged rows. The conceptual move: Todo doesn’t update anything itself — it builds the object it wants to exist and asks the parent to apply it. The parent owns the array, so the parent owns the update.
What you should see now
Clicking any checkbox flips it. Click again — flips back. The state is real: open React DevTools, watch TodoList’s State change as you click.
Add a new todo via the composer, then check it off — the new one toggles too. That’s because handleUpdateTodo finds the right item by id, regardless of where it came from.
Next: deletion.