Updating objects and arrays in state
Change objects and arrays in state the React way, by creating new copies instead of mutating the old ones.
So far your state has held simple values like numbers, strings and booleans. Real apps keep lists and records in state: a list of books, a user's profile, a shopping cart. Those are arrays and objects.
There's one rule to learn here, and it's the most common source of beginner bugs in React: treat objects and arrays in state as read-only. When you want to change one, make a new copy with the change and set that.
The bug: mutating state
Here's a form that edits a person's name. It changes the object directly. Type in the boxes and watch the greeting underneath. It never changes.
Changing a property on an existing object, like person.firstName = 'Grace', is called mutation. Mutation doesn't call a setter, so React never hears about it and never re-renders.
Even if you call the setter with the same mutated object, React compares the old and new value with Object.is. It's the same object, so React decides nothing changed and skips the render. React only notices a change when you give it a different object.
Copying objects with spread
The fix is to create a new object. The spread syntax ... (from Day 1) copies all the properties of an existing object into a new one. Then you list the properties you want to change after it, and they override the copied ones.
{ ...person, firstName: 'Grace' } means "a new object with everything from person, but with firstName set to 'Grace'." The other two fields are copied over unchanged.
This example uses one more trick. The square brackets in [e.target.name]: e.target.value are a computed property name: the key comes from a variable. Because each input has a name that matches a field, one handler can update any of them.
Nested objects
Spread is shallow: it copies one level. If your object contains another object, you need to copy each level you change.
const [book, setBook] = useState({
title: 'Dune',
details: { pages: 412, year: 1965 },
});
// Change details.year: copy the outer object AND the inner one
setBook({
...book,
details: {
...book.details,
year: 1966,
},
});If that feels verbose, it is. It's one reason to keep state flat where you can, which you'll explore on Day 4. Many teams also use a small library called Immer for deeply nested updates; see Go deeper.
Updating arrays
Arrays in state follow the same rule: don't change them in place, produce a new array. Some array methods mutate and some return a new array. Here's a cheat sheet:
| Goal | Avoid (mutates) | Prefer (returns a new array) |
|---|---|---|
| Add an item | push, unshift | [...arr, item], [item, ...arr] |
| Remove an item | splice, pop, shift | filter |
| Change an item | arr[i] = x | map |
| Sort or reverse | sort, reverse | toSorted, toReversed, or copy first: [...arr].sort() |
The three you'll use constantly are spread to add, filter to remove and map to change. Here they are together in a small to-do list.
Look closely at handleToggle. map builds a new array. For the matching todo it returns a new object with done flipped. For every other todo it returns the same object unchanged. That's the pattern for changing one item in a list, and it combines both halves of this lesson: a new array, and a new object for the item that changed.
Deep diveWhy does React care so much about immutability?
Three reasons:
- Change detection is cheap. React checks whether state changed by comparing references (
Object.is(oldValue, newValue)). That's instant. If you mutated, React would have to dig through every property to find differences. - Snapshots stay correct. In the previous lesson you saw that each render has its own snapshot of state. If you mutate an object, you also change the "old" snapshot, and old renders and handlers suddenly see values they shouldn't.
- Features rely on it. Undo/redo, comparing before and after, and performance tools like
memo(and the React Compiler on Day 6) all assume old values never change.
Preview: cycling a book's status
In today's project checkpoint, each book has a status of 'want', 'reading' or 'done', and a button moves it to the next one. Everything you need is in this lesson: a small helper to pick the next status, plus map with spread.
Shopping cart
Finish the cart. The + button should increase that item's count. The – button should decrease it, and if the count would drop to zero, remove the item from the cart instead. Don't mutate anything.
Show solution
map builds a new array with a new object for the changed item. Chaining filter afterwards drops any item whose count reached zero. Both return new arrays, so nothing in the old state is touched.
Which update correctly removes the book with id '3' from books in state?
You run person.age = 31 and then setPerson(person). What happens?
Day 3 progress