On this page

Immutable Array Methods

3 min read

In the last topic we saw that React needs a new reference to notice a change. For arrays, that means choosing methods that return a new array instead of changing the old one.

Definition

A mutating method changes the original array. A non-mutating (immutable) method leaves the original alone and returns a new one.

TaskMutates (avoid on state)Does not mutate (use this)
Add to endpush[...arr, item]
Add to startunshift[item, ...arr]
Removesplice, pop, shiftfilter, slice
Replace onearr[i] = xmap, with
SortsorttoSorted, or [...arr].sort()
ReversereversetoReversed, or [...arr].reverse()
Insert in the middlesplicetoSpliced, or slice + spread

Analogy: editing a document with tracked copies. A mutating method overwrites the original file. A non-mutating one saves a new version and keeps the old.

The Everyday Toolkit

js
1const todos = [
2 { id: 1, text: "Learn", done: false },
3 { id: 2, text: "Build", done: false },
4];
5
6// Add
7const added = [...todos, { id: 3, text: "Ship", done: false }];
8
9// Remove by id
10const removed = todos.filter((t) => t.id !== 2);
11
12// Update one item
13const toggled = todos.map((t) =>
14 t.id === 1 ? { ...t, done: true } : t
15);
16
17// Insert at position 1
18const inserted = [...todos.slice(0, 1), newTodo, ...todos.slice(1)];

Note the map example. Only the changed item becomes a new object. Unchanged items keep their old reference, which helps React.memo skip them.

Sorting Is a Trap

sort mutates the array and returns the same array.

js
1const [items, setItems] = useState([3, 1, 2]);
2
3items.sort(); // ✗ mutates state in place
4setItems(items); // same reference → no re-render
5
6setItems([...items].sort((a, b) => a - b)); // ✓ copy first, then sort
7setItems(items.toSorted((a, b) => a - b)); // ✓ modern, one step

Also remember that default sort() compares values as strings: [10, 9, 1].sort() gives [1, 10, 9]. Always pass a compare function for numbers.

Newer Methods

Recent JavaScript added non-mutating versions of the old mutating methods:

js
1arr.toSorted(fn) // copy of sort
2arr.toReversed() // copy of reverse
3arr.toSpliced(i, n, ...x) // copy of splice
4arr.with(i, value) // copy with one index replaced

Check browser support for your targets. Older environments need the spread-and-copy versions.

Objects and Nested Data

The same idea applies to objects. Spread every level you change.

js
1const next = {
2 ...state,
3 user: {
4 ...state.user,
5 address: { ...state.user.address, city: "Delhi" },
6 },
7};

This gets painful with deep data. Tools like Immer let you write mutating-looking code and produce a safe copy. Redux Toolkit uses Immer inside createSlice.

Here Is What Breaks

js
1const copy = items; // not a copy, same reference
2copy.push(4); // mutates items too ✗

Another one:

js
1const copy = [...items]; // shallow copy
2copy[0].done = true; // ✗ objects inside are still shared

In React

Use these every day in state updates:

jsx
1setTodos((prev) => [...prev, newTodo]); // add
2setTodos((prev) => prev.filter((t) => t.id !== id)); // remove
3setTodos((prev) => prev.map((t) => t.id === id ? { ...t, done: !t.done } : t)); // update

Using the functional updater (prev => ...) also protects you from stale state when several updates happen together.

Summary

1Mutating → push, pop, shift, unshift, splice, sort, reverse, index assignment
2Immutable → spread, filter, map, slice, toSorted, toReversed, toSpliced, with
3Add → spread Remove → filter Update → map + spread
4sort and reverse → copy first, or use the "to" versions
5Nested data → copy every level you change, or use Immer
6React → new array = re-render, same array = nothing happens

Spread showed up in almost every example above. The next topic covers it, along with destructuring and rest.

Next TopicDestructuring, Spread & Rest