Command Palette

Search for a command to run...

Hectal
PHASE 4Beginner ~22 min· topic 7 of 8

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, splice or sort.
Copying method
An array method that returns a new array and leaves the original alone, like map, filter or toSorted.
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 change arr.
toSorted / toReversed
Copying versions of sort and reverse (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.

List.jsxwhole filejsx
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.

List.jsxwhole filejsx
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.

List.jsxwhole filejsx
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.

List.jsxwhole filejsx
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.

List.jsxwhole filejsx
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.

Pick the right tooldiagram
Rendering diagram…

Try it yourself

  1. 1

    Add to the start

    In the first example, add a button that puts Raja Ravi Varma at the start of the list. Predict its id and position in the console line.

  2. 2

    Sort by name

    Add a Sort A–Z button to the first example using artists.toSorted((a, b) => a.name.localeCompare(b.name)). Because each <li> has a stable key, React moves the existing elements instead of rebuilding them.

  3. 3

    Fix the bad toggle

    Rewrite badToggle with map and an object spread. Run again: ticking your own list must no longer touch the other one.

Code & diagrams

Add, remove, replace and insert New tab

Each line is one render. Every operation built a new array; none of them used `push`, `splice` or `arr[i] =`.

Preview

Press Run to see the page.

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() mutates and is ignored; toSorted() works New tab

`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

Press Run to see the page.

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 ]
Arrays of objects: copy the item you change New tab

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

Press Run to see the page.

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); };.

terminal
$ Preview and console
── what you'll see ──
Clicking Add changes nothing on screen and logs no new render. The item appears later, when some other update happens.

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.

terminal
$ Preview
── what you'll see ──
Sometimes the wrong item disappears.

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, toSpliced and with are ES2023 "change array by copy" methods, supported in all current browsers. For older targets, use [...arr].sort() or a polyfill.

  • ▸

    map that 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 an Intl.Collator) sorts words the way people expect in their language; the default sort() 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. 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. 2

    Mutating methods (avoid on state): push, unshift, pop, shift, splice, sort, reverse, fill, copyWithin, and arr[i] = x. Copying methods (use these): spread [...arr, x], concat, filter, map, slice, and the newer toSorted, toReversed, toSpliced and with(index, value), which are copying versions of sort, reverse, splice and arr[i] = x (in all current browsers since 2023; see the /javascript course on arrays). Note the trap: slice copies, splice mutates.

  3. 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) or items.toReversed() (or copy first: [...items].sort(compare), and then mutating the fresh copy is fine).

  4. 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 is map plus 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. 5

    Use ids, not positions. Identify items by a stable id (and use it as the key, 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 or crypto.randomUUID().

  6. 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

01

Which array methods should you avoid on state, and what do you use instead?

02

How do you insert an item at position i without mutating?

03

Why can [...list] followed by copy[0].done = true still be a mutation bug?

04

Why should list items be identified by id instead of index?

Practice

01

State todos is [{ id, text, done }]. Write the three handlers addTodo(text), toggleTodo(id) and deleteTodo(id) without mutating.

02

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/toReversed read 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/toReversed or a copy.

  • I can change one object inside an array with map and spread.

  • I can tell mutating methods from copying ones.

  • I identify items by stable ids, not indexes.