The flow: add, list, mark, delete
In React data flows downward (from parents to children, via props) and the events go up (a child notifies the parent by calling a function the parent passed it). Let's see our app's full cycle.
1. State lives in App
function App() {
const [tasks, setTasks] = useState([]);
const [text, setText] = useState("");
// ...
}
tasks: the array of tasks.text: what the user is typing (controlled input).
2. Add with a controlled input + button
An input is controlled when its value is set by the state and every keystroke
updates that state with onChange. When clicking "Add", we create a new task
and concatenate it without mutating the previous array:
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={add}>Add</button>
function add() {
if (text.trim() === "") return; // don't add empty ones
const newTask = { id: Date.now(), text, done: false };
setTasks([...tasks, newTask]); // copy + the new one
setText(""); // clear the input
}
Never do
tasks.push(...): mutating the array doesn't notify React. Create a new one with[...tasks, newTask]and pass it tosetTasks.
3. List (pass the array downward)
<TaskList tasks={tasks} />
And inside, one <li> per task, with a stable key (the id):
function TaskList({ tasks }) {
return (
<ul>
{tasks.map((t) => (
<li key={t.id}>
<TaskItem task={t} />
</li>
))}
</ul>
);
}
4. Mark as done (events go up)
To mark a task, the parent defines how it's done and the children only
notify. We map the array and return a copy with done toggled on the
correct task:
function complete(id) {
setTasks(tasks.map((t) =>
t.id === id ? { ...t, done: !t.done } : t
));
}
With these pieces you have a complete app. In the exercises you'll build it
from the bottom up: first TaskItem, then TaskList and finally the
App that ties it all together.