Updating Arrays in State
Arrays follow the exact same rule as objects: treat them as read-only, and to change one, build a new array and set state to that. The only new thing to learn here is which array methods are safe, because JavaScript has two kinds. Some mutate the array in place (off-limits) and some return a new array (what you want).
Updating arrays without mutation
The trick is to avoid the mutating methods and reach for the ones that return a fresh array. This table is worth memorizing, it covers almost everything you'll do.
| Operation | Avoid (mutates the array) | Prefer (returns a new array) |
|---|---|---|
| adding | push, unshift | concat, [...arr] spread |
| removing | pop, shift, splice | filter, slice |
| replacing | splice, arr[i] = ... | map |
| sorting | reverse, sort | copy the array first |
Pitfall: slice and splice look alike but do opposite things. slice copies an array or part of it and returns a new one. splice mutates the array to insert or delete. In React you'll use slice (no p) far more, because you never want to mutate state. See Updating Objects for why mutation is off-limits.
Adding to an array
Spread the old array into a new one and add the item. Where you put the spread decides whether the item lands at the end or the start.
setArtists([
...artists, // everything that was there
{ id: nextId++, name: name }, // new item at the end
]);
setArtists([
{ id: nextId++, name: name }, // new item at the start
...artists,
]);
This replaces push (adds to end) and unshift (adds to start), both of which mutate.
Removing from an array
filter builds a new array containing only the items you want to keep, which is how you remove.
setArtists(
artists.filter((a) => a.id !== artist.id) // keep everyone except this id
);
Transforming an array
When you want to change some or all items, map returns a new array where you decide what each item becomes.
const nextShapes = shapes.map((shape) => {
if (shape.type === "square") {
return shape; // leave squares untouched
}
return { ...shape, y: shape.y + 50 }; // move everything else down
});
setShapes(nextShapes);
Replacing items in an array
Replacing is just map with a condition, usually matching by index or id, returning the new value for the match and the old value for everything else.
const nextCounters = counters.map((c, i) => {
if (i === index) {
return c + 1; // the one we're changing
}
return c; // the rest, unchanged
});
setCounters(nextCounters);
Inserting into an array
To drop an item at a specific spot (not just the ends), slice the array around that spot and spread the pieces with the new item in the middle.
const insertAt = 1;
const nextArtists = [
...artists.slice(0, insertAt), // items before the spot
{ id: nextId++, name: name }, // the new item
...artists.slice(insertAt), // items after the spot
];
setArtists(nextArtists);
Making other changes to an array
Some operations, like reverse and sort, have no non-mutating twin. For those, make a copy first, then mutate the copy. The copy is a fresh local array, so mutating it is safe (this is the "local mutation" idea again).
const nextList = [...list]; // copy first
nextList.reverse(); // now mutate the copy
setList(nextList);
One catch: copying an array with [...list] is shallow, so the objects inside are still the same objects. That leads straight to the next section.
Updating objects inside arrays
An object inside an array isn't really "inside" it, the array just holds a reference to it. So copying the array doesn't copy those objects, and mutating one still corrupts state.
const nextList = [...list];
nextList[0].seen = true; // ❌ still mutates the original list[0] object
setList(nextList);
The fix is to use map and, for the item you're changing, return a new object with the spread rather than editing the old one.
setMyList(myList.map((artwork) => {
if (artwork.id === artworkId) {
return { ...artwork, seen: nextSeen }; // new object for the changed item
}
return artwork; // others stay as-is
}));
Write concise update logic with Immer
Just like with objects, deep array updates get wordy, and Immer lets you write mutation-style code on a draft while it produces the immutable result for you.
import { useImmer } from "use-immer";
const [myList, updateMyList] = useImmer(initialList);
function handleToggleMyList(id, nextSeen) {
updateMyList((draft) => {
const artwork = draft.find((a) => a.id === id);
artwork.seen = nextSeen; // safe: it's a draft, not real state
});
}
Recap
You can keep arrays in state, but you can't change them in place. Instead of mutating, create a new array and set state to it. Use [...arr, newItem] spread to add, filter to remove, and map to transform or replace. For operations with no immutable version, like reverse or sort, copy the array first and mutate the copy. Remember the copy is shallow, so to change an object inside an array you must also make a new object for that item (via map and spread). And when all this copying gets repetitive, Immer keeps it concise.