Immutable Array Methods
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.
| Task | Mutates (avoid on state) | Does not mutate (use this) |
|---|---|---|
| Add to end | push | [...arr, item] |
| Add to start | unshift | [item, ...arr] |
| Remove | splice, pop, shift | filter, slice |
| Replace one | arr[i] = x | map, with |
| Sort | sort | toSorted, or [...arr].sort() |
| Reverse | reverse | toReversed, or [...arr].reverse() |
| Insert in the middle | splice | toSpliced, 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
1const todos = [2 { id: 1, text: "Learn", done: false },3 { id: 2, text: "Build", done: false },4];5 6// Add7const added = [...todos, { id: 3, text: "Ship", done: false }];8 9// Remove by id10const removed = todos.filter((t) => t.id !== 2);11 12// Update one item13const toggled = todos.map((t) =>14 t.id === 1 ? { ...t, done: true } : t15);16 17// Insert at position 118const 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.
1const [items, setItems] = useState([3, 1, 2]);2 3items.sort(); // ✗ mutates state in place4setItems(items); // same reference → no re-render5 6setItems([...items].sort((a, b) => a - b)); // ✓ copy first, then sort7setItems(items.toSorted((a, b) => a - b)); // ✓ modern, one stepAlso 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:
1arr.toSorted(fn) // copy of sort2arr.toReversed() // copy of reverse3arr.toSpliced(i, n, ...x) // copy of splice4arr.with(i, value) // copy with one index replacedCheck 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.
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
1const copy = items; // not a copy, same reference2copy.push(4); // mutates items too ✗Another one:
1const copy = [...items]; // shallow copy2copy[0].done = true; // ✗ objects inside are still sharedIn React
Use these every day in state updates:
1setTodos((prev) => [...prev, newTodo]); // add2setTodos((prev) => prev.filter((t) => t.id !== id)); // remove3setTodos((prev) => prev.map((t) => t.id === id ? { ...t, done: !t.done } : t)); // updateUsing 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 assignment2Immutable → spread, filter, map, slice, toSorted, toReversed, toSpliced, with3Add → spread Remove → filter Update → map + spread4sort and reverse → copy first, or use the "to" versions5Nested data → copy every level you change, or use Immer6React → new array = re-render, same array = nothing happensSpread showed up in almost every example above. The next topic covers it, along with destructuring and rest.