Skip to content

React Tutorial State and Events

Updating Arrays in State: Add, Remove, Replace

Lesson 55 of 156 2 min read By Deepak Paswan Updated Oct 10, 2026

Runnable lesson · press Try it on the examples to edit and run them here.

Arrays in state follow the same rule as objects: never change them in place. Create a new array and pass it to the setter. The trick is knowing which array methods are safe.

Safe and unsafe methods

ActionAvoid (mutates)Prefer (returns a new array)
Addpush, unshift[...arr, item], [item, ...arr], arr.concat(item)
Removepop, shift, splicearr.filter(...), arr.slice(...)
Replacearr[i] = xarr.map(...)
Sort / reversesort, reverseCopy first: [...arr].sort(), or arr.toSorted()
Insertsplice[...arr.slice(0, i), item, ...arr.slice(i)]

(toSorted, toReversed, toSpliced and with are newer non-mutating methods; check feature support if you must support older browsers.) See also Immutable Array Methods.

Add, remove and update: a todo list

This example uses every pattern:

App.jsx
JSX
function App() {
  const [todos, setTodos] = React.useState([
    { id: 1, text: "Learn React", done: false },
    { id: 2, text: "Walk dog", done: false },
  ]);
  const [text, setText] = React.useState("");

  function addTodo() {
    if (!text.trim()) return;
    // ADD: a new array with the old items plus the new one
    setTodos([...todos, { id: Date.now(), text: text.trim(), done: false }]);
    setText("");
  }

  function toggleTodo(id) {
    // REPLACE one item: map, copy only the item that changes
    setTodos(todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
  }

  function removeTodo(id) {
    // REMOVE: keep everything except that id
    setTodos(todos.filter((t) => t.id !== id));
  }

  const doneCount = todos.filter((t) => t.done).length;

  return (
    <div>
      <input aria-label="New todo" value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={addTodo}>Add</button>
      <p>Todos: {todos.length}, Done: {doneCount}</p>
      <ul>
        {todos.map((t) => (
          <li key={t.id}>
            <span style={{ textDecoration: t.done ? "line-through" : "none" }}>{t.text}</span>{" "}
            <button onClick={() => toggleTodo(t.id)} aria-label={"Toggle " + t.text}>Toggle {t.text}</button>{" "}
            <button onClick={() => removeTodo(t.id)} aria-label={"Remove " + t.text}>Remove {t.text}</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

Each setter call builds a new array, and toggleTodo also builds a new object for the one item that changed ({ ...t, done: !t.done }). The other items are reused as they are.

Nested objects in arrays

Because spread is shallow, { ...t } copies the todo but shares any nested objects. If a todo had a tags array, changing it would need { ...t, tags: [...t.tags, "new"] }.

Inserting at a position

App.jsx
JSX
function App() {
  const [letters, setLetters] = React.useState(["a", "b", "d"]);

  function insertC() {
    const index = 2;
    setLetters([...letters.slice(0, index), "c", ...letters.slice(index)]);
  }

  return (
    <div>
      <p>{letters.join(", ")}</p>
      <button onClick={insertC}>Insert c</button>
    </div>
  );
}

Reordering and sorting

App.jsx
JSX
function App() {
  const [nums, setNums] = React.useState([3, 1, 2]);
  return (
    <div>
      <p>{nums.join(", ")}</p>
      <button onClick={() => setNums([...nums].sort((a, b) => a - b))}>Sort</button>
    </div>
  );
}

Spread first ([...nums]) and then sort the copy. nums.sort(...) would sort the state array in place.

Do you need to store the sorted or filtered version?

No. Keep the original array in state and derive the sorted or filtered list while rendering, as shown in Filtering and Sorting Lists.

Key points

  • Add with spread, remove with filter, update with map, and sort a copy.
  • Replace only the objects that change; reuse the rest.
  • Use ids, not array positions, to say which item to change.