Skip to content
React rc state-events 4 min read

Updating Arrays in State

Arrays are one of the most common shapes of React state—todo lists, search results, selected tags, cart items. But arrays are objects, and like every value you store in state, React expects you to treat them as immutable. Instead of changing the array in place, you build a new array and hand it to the setter. This page covers the immutable recipes for adding, removing, updating, inserting, and sorting items, and shows exactly which built-in methods to avoid.

Why you can’t mutate arrays in state

When you call arr.push(item), you change the existing array object but its identity—the reference—stays the same. React compares the previous and next state by reference (Object.is), so a mutated-in-place array looks unchanged, and your component may not re-render. Mutation also breaks features that rely on previous state, like time-travel debugging and StrictMode checks.

The rule of thumb: avoid the methods that mutate, prefer the ones that return a new array.

OperationAvoid (mutates)Prefer (returns a copy)
Addpush, unshift[...arr, item], concat
Removepop, shift, splicefilter, slice
Replacearr[i] = x, splicemap
Sortsort, reversetoSorted, toReversed, or [...arr].sort()

Adding items

Use the spread operator to create a new array containing the old items plus the new one. Spreading at the end appends; spreading at the start prepends.

import { useState } from "react";

function TagList() {
  const [tags, setTags] = useState(["react", "state"]);

  function addTag(name) {
    setTags((prev) => [...prev, name]);      // append
    // setTags((prev) => [name, ...prev]);   // prepend
  }

  return (
    <div>
      <button onClick={() => addTag("hooks")}>Add tag</button>
      <p>{tags.join(", ")}</p>
    </div>
  );
}

Output:

// After clicking "Add tag":
react, state, hooks

The updater form prev => [...prev, name] is important here: it guarantees you append to the latest array even if several adds happen in the same event.

Removing items

filter returns a new array containing only the items that pass a test, which makes it the natural tool for removal. Match items by a stable id rather than by index, since indexes shift as the list changes.

function removeTag(idToRemove) {
  setTags((prev) => prev.filter((tag) => tag.id !== idToRemove));
}

If you only know the position, slice (not splice) copies a range without mutating:

function removeAt(index) {
  setTags((prev) => [
    ...prev.slice(0, index),
    ...prev.slice(index + 1),
  ]);
}

Updating items

To change one item, use map. It walks the array and returns a new one; for the item you want to change, return a fresh object, and for everything else return it unchanged.

function toggleDone(id) {
  setTodos((prev) =>
    prev.map((todo) =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    )
  );
}

Spreading the matched object ({ ...todo, done: !todo.done }) keeps the other fields and avoids mutating the original—this is the same immutable-update pattern you use for objects.

Returning the same object for unchanged items is intentional and efficient. Only the one item you edited gets a new reference, which lets React (and memoized children) skip work for the rest.

Inserting at a position

Inserting combines slice and spread: copy everything before the index, drop in the new item, then copy everything after.

function insertAt(index, item) {
  setItems((prev) => [
    ...prev.slice(0, index),
    item,
    ...prev.slice(index),
  ]);
}

Sorting and reversing safely

sort and reverse mutate the array in place, so calling them on state is a bug. Either copy first with spread, or use the modern non-mutating methods toSorted and toReversed (available in modern browsers and Node 20+).

// Copy-then-sort (works everywhere):
setItems((prev) => [...prev].sort((a, b) => a.price - b.price));

// Modern non-mutating equivalent:
setItems((prev) => prev.toSorted((a, b) => a.price - b.price));
setItems((prev) => prev.toReversed());
const original = [3, 1, 2];
const sorted = original.toSorted((a, b) => a - b);
console.log(original); // unchanged
console.log(sorted);   // new sorted array

Output:

[3, 1, 2]
[1, 2, 3]

For deeply nested arrays of objects, copying the outer array is not enough—you must also copy the nested objects you change, exactly as map does above. For deep updates, a library like Immer (useImmer) lets you write apparently-mutating code that produces an immutable result.

Best Practices

  • Never call push, pop, shift, unshift, splice, sort, or reverse directly on state arrays.
  • Add with spread ([...prev, item]), remove with filter, and replace with map.
  • Use the updater form set(prev => ...) so updates build on the latest array.
  • Match items by a stable id, not by array index, since indexes shift on insert and remove.
  • When updating an item, spread it into a new object instead of mutating the existing one.
  • Prefer toSorted/toReversed, or copy with [...arr] before sorting, to keep state untouched.
  • For complex nested arrays, reach for Immer (useImmer) rather than hand-writing deep copies.
Last updated June 14, 2026
Was this helpful?