Topic 4.7
Updating Arrays in State
In one line
Arrays in state are read-only too: add, remove, replace, insert, reorder and sort by building a new array with spread, filter, map, slice or the copying methods toSorted and toReversed, and copy any object inside an array before changing it.
Think of it like this
A class attendance list printed on paper. When a new child joins, the teacher doesn't squeeze a name into the old sheet; the office prints a new sheet with the extra name. The old sheet is still in the file, unchanged. React wants arrays handled the same way: every change produces a new array.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Mutating method
- An array method that changes the array itself, like
push,spliceorsort. - Copying method
- An array method that returns a new array and leaves the original alone, like
map,filterortoSorted. - filter
- Returns a new array with only the items that pass a test. Used to remove items.
- map
- Returns a new array with every item transformed. Used to change one or more items.
- slice
- Returns a copy of part of an array:
arr.slice(start, end). Doesn't changearr. - toSorted / toReversed
- Copying versions of
sortandreverse(ES2023): they return a new array. - Stable id
- A value that identifies an item and never changes, even when the list is reordered.
Step by step
01Add an item
Make a new array with all the old items plus the new one. push would change the array React is holding.
let nextId = 3;
function handleAdd(name) {
setArtists([...artists, { id: nextId++, name }]); // ✅ new array
// artists.push({ id: nextId++, name }); // ❌ mutation
}02Remove an item
filter keeps every item except the one you want gone.
setArtists(artists.filter((a) => a.id !== id));03Replace or change an item
map returns a new array. Return a new object for the item you change and the same object for the rest.
setArtists(
artists.map((a) => (a.id === id ? { ...a, name: newName } : a)),
);04Insert at a position
Slice the array into the part before and the part after, and put the new item between them.
const i = 1; // insert as the second item
setArtists([
...artists.slice(0, i),
newArtist,
...artists.slice(i),
]);05Sort and reverse without mutating
toSorted and toReversed return new arrays. On older code you'll see [...arr].sort(...): copy first, then sorting the copy is fine.
setArtists(artists.toSorted((a, b) => a.name.localeCompare(b.name)));
setArtists(artists.toReversed());
// same result, older style:
setArtists([...artists].sort((a, b) => a.name.localeCompare(b.name)));06Pick the right tool
Every change to an array in state has a copying recipe.
Try it yourself
- 1
Add to the start
In the first example, add a button that puts
Raja Ravi Varmaat the start of the list. Predict its id and position in the console line. - 2
Sort by name
Add a
Sort A–Zbutton to the first example usingartists.toSorted((a, b) => a.name.localeCompare(b.name)). Because each<li>has a stablekey, React moves the existing elements instead of rebuilding them. - 3
Fix the bad toggle
Rewrite
badTogglewithmapand an object spread. Run again: ticking your own list must no longer touch the other one.
Code & diagrams
Each line is one render. Every operation built a new array; none of them used `push`, `splice` or `arr[i] =`.
Preview
Console
Nothing logged yet.Expected console output
0:Amrita Sher-Gil, 1:M. F. Husain, 2:S. H. Raza
0:Amrita Sher-Gil, 1:M. F. Husain, 2:S. H. Raza, 3:Tyeb Mehta
0:Amrita Sher-Gil, 2:S. H. Raza, 3:Tyeb Mehta
0:Amrita, 2:S. H. Raza, 3:Tyeb Mehta
0:Amrita, 4:Jamini Roy, 2:S. H. Raza, 3:Tyeb Mehta`sort()` reordered the state array secretly, but the screen still showed the old order. The next real update (reverse) then revealed the hidden change: the reverse is of the sorted array.
Preview
Console
Nothing logged yet.Expected console output
render: [ 42, 7, 99, 15 ]
after sort(), the array is [ 7, 15, 42, 99 ] but no render happened
screen shows: 42 · 7 · 99 · 15
render: [ 99, 42, 15, 7 ]The bad button ticked Big Bellies in **my** list only, yet it shows as seen in **both** lists. The bad toggle copied the array but mutated an object both lists share. The good toggle made a new object for the one item, so only your list got Lunar Landscape.
Preview
Console
Nothing logged yet.Expected console output
Mine: Big Bellies ✓, Lunar Landscape | Yours: Big Bellies ✓, Lunar Landscape
Mine: Big Bellies ✓, Lunar Landscape | Yours: Big Bellies ✓, Lunar Landscape ✓Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
push into state
In the first example, write const add = () => { artists.push({ id: nextId++, name: "Tyeb Mehta" }); setArtists(artists); };.
Break #2
Remove by index after the list changed
Delete buttons call setItems(items.filter((_, i) => i !== index)) with an index captured when the list had a different order.
Myth vs fact
Myth
[...arr] is a deep copy.
Fact
It copies the array, not the objects inside. Changing copy[0].done changes the original's object too.
Myth
slice and splice are the same.
Fact
slice returns a copy of part of the array and leaves it unchanged. splice removes or inserts items in place: avoid it on state, or use toSpliced.
Myth
Copying arrays on every change will make large lists slow.
Fact
Copying an array of references is fast even for thousands of items; it's the rendering of many rows that costs, which keys, memo and virtualization handle (Phase 16).
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
toSorted,toReversed,toSplicedandwithare ES2023 "change array by copy" methods, supported in all current browsers. For older targets, use[...arr].sort()or a polyfill. - ▸
mapthat returns the same object for unchanged items preserves their references, so memoized row components (memo, Phase 16) skip rendering everything except the changed row. - ▸
Sorting with
localeCompare(or anIntl.Collator) sorts words the way people expect in their language; the defaultsort()compares UTF-16 code units, so"Zoya"comes before"amit". - ▸
For very frequent list updates (chat, logs), store items in an object keyed by id plus an array of ids (normalized state, Topic 6.1); updating one item then touches one entry instead of mapping the whole list.
Remember this
- 1
Arrays are objects, so everything from Topic 4.6 applies: React compares the old and new array with
Object.is, by reference. If you change the array in place and pass it back, React sees the same array and doesn't re-render. So in React you avoid the mutating methods and prefer the copying ones. - 2
Mutating methods (avoid on state):
push,unshift,pop,shift,splice,sort,reverse,fill,copyWithin, andarr[i] = x. Copying methods (use these): spread[...arr, x],concat,filter,map,slice, and the newertoSorted,toReversed,toSplicedandwith(index, value), which are copying versions ofsort,reverse,spliceandarr[i] = x(in all current browsers since 2023; see the /javascript course on arrays). Note the trap:slicecopies,splicemutates. - 3
The recipes. Add to the end:
[...items, newItem]. Add to the start:[newItem, ...items]. Remove:items.filter((it) => it.id !== id). Replace or change one item:items.map((it) => (it.id === id ? newValue : it)). Insert at a position:[...items.slice(0, i), newItem, ...items.slice(i)]. Sort or reverse:items.toSorted(compare)oritems.toReversed()(or copy first:[...items].sort(compare), and then mutating the fresh copy is fine). - 4
Arrays of objects. Copying an array is shallow: the new array holds the same objects. So
const next = [...items]; next[0].seen = true;still mutates an object the old state shares. If the same object also appears in another list, both lists change. The fix ismapplus an object spread:items.map((it) => (it.id === id ? { ...it, seen: true } : it)): a new array, a new object for the changed item, and the unchanged items shared. - 5
Use ids, not positions. Identify items by a stable
id(and use it as thekey, Topic 3.3), not by index. Indexes shift when you insert or remove, so code like "remove item 2" can delete the wrong one after the list changes. Generate ids when items are created, for example with a counter orcrypto.randomUUID(). - 6
Keep state minimal: the visible list after filtering or sorting is usually calculated during render from the stored list (
const visible = todos.filter(...)), not stored as a second array that must be kept in sync (Topic 6.2). When arrays become large and updates complex, a reducer (Topic 9.4) or Immer (Topic 4.6) keeps the code readable.
Explain it without notes
Which array methods should you avoid on state, and what do you use instead?
How do you insert an item at position i without mutating?
Why can [...list] followed by copy[0].done = true still be a mutation bug?
Why should list items be identified by id instead of index?
Practice
State todos is [{ id, text, done }]. Write the three handlers addTodo(text), toggleTodo(id) and deleteTodo(id) without mutating.
Write a handler that moves the item with a given id one place up in the list (do nothing if it's already first).
Trade-offs
- ↔
toSorted/toReversedread clearly but need ES2023;[...arr].sort()works everywhere and is just as safe. - ↔
Mapping the whole array to change one item is simple and fast for normal lists; for huge, frequently updated collections, a normalized shape (Topic 6.1) or Immer is easier on both code and CPU.
Done when you can
I can add, remove, replace and insert items without mutating.
I can sort and reverse with
toSorted/toReversedor a copy.I can change one object inside an array with
mapand spread.I can tell mutating methods from copying ones.
I identify items by stable ids, not indexes.