Treat state as read-only
Do not mutate an object or array already stored in state. Create a new value and pass it to the setter. This preserves previous render snapshots and gives React a clear signal that the value changed.
Update an array
setTasks((current) => [ ...current, { id: crypto.randomUUID(), title: newTitle, complete: false } ]); setTasks((current) => current.filter((task) => task.id !== id ) );
Use map to replace or transform items, filter to remove them and spread syntax to add them. These methods return new arrays.
Update an object
setTasks((current) => current.map((task) => task.id === id ? { ...task, complete: !task.complete } : task ) );
The spread is shallow. If nested data must change, copy each object along the path, or consider a flatter state shape.
Why mutation causes trouble
tasks.push(newTask) changes the existing array and returns its length, not a new array. Changing task.complete also edits an object used by an earlier render. New arrays and objects preserve each render's snapshot and make changed references clear.
The toggle example maps every item. It copies the matching object with one changed property and reuses all unchanged objects inside a new array.
Challenge: edit a task title
Write renameTask(id, newTitle) using a functional state update and map. Replace only the matching task and preserve its other properties.
setTasks(current => current.map(task =>
task.id === id ? { ...task, title: newTitle } : task
));
