Skip to main content

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.

OperationAvoid (mutates the array)Prefer (returns a new array)
addingpush, unshiftconcat, [...arr] spread
removingpop, shift, splicefilter, slice
replacingsplice, arr[i] = ...map
sortingreverse, sortcopy 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.

add-item.jsx
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.

remove-item.jsx
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.

transform.jsx
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.

replace.jsx
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.

insert.jsx
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).

reverse.jsx
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.

nested-bug.jsx
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.

nested-fixed.jsx
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.

with-immer.jsx
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.