Updating objects and arrays

React · Essentials · 12 min

Mutating state is the classic cause of 'my UI doesn't update'; immutable updates are the fix and a core React habit.

State can hold objects and arrays, but you must treat them as immutable. React decides whether to re-render by comparing the old and new value with Object.is. If you mutate the existing object and pass the same reference back, React sees no change, and even if something else triggers a render, you've corrupted the previous snapshot.

So instead of changing data, create a new copy with the change applied.

Objects: spread the old fields, then override.

setPerson({ ...person, email: newEmail });
setPerson({ ...person, address: { ...person.address, city } }); // nested: copy each level

Arrays: use methods that return a new array.

  • Add: [...items, item] or [item, ...items]
  • Remove: items.filter(i => i.id !== id)
  • Replace or update one: items.map(i => i.id === id ? { ...i, done: true } : i)
  • Insert: [...items.slice(0, n), item, ...items.slice(n)]
  • Sort/reverse: copy first, [...items].sort(cmp), or use toSorted() / toReversed()

Avoid push, splice, sort, reverse and direct assignment (items[0] = x, person.name = x) on state.

If deeply nested updates get painful, that's often a sign the state should be flattened (store items by id, keep selectedId rather than a copy of the selected object). The Immer library (useImmer) lets you write mutating-looking code that produces immutable copies, which some teams prefer for complex structures.

Example

import { useState } from "react";

let nextId = 3;

export default function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, title: "Buy milk", done: false },
    { id: 2, title: "Write tests", done: true },
  ]);
  const [title, setTitle] = useState("");

  const add = () => {
    setTodos([...todos, { id: nextId++, title, done: false }]);
    setTitle("");
  };
  const toggle = id =>
    setTodos(todos.map(t => (t.id === id ? { ...t, done: !t.done } : t)));
  const remove = id => setTodos(todos.filter(t => t.id !== id));

  return (
    <>
      <input value={title} onChange={e => setTitle(e.target.value)} />
      <button onClick={add} disabled={!title}>Add</button>
      <ul>
        {todos.map(t => (
          <li key={t.id}>
            <input type="checkbox" checked={t.done} onChange={() => toggle(t.id)} />
            {t.title} <button onClick={() => remove(t.id)}>x</button>
          </li>
        ))}
      </ul>
    </>
  );
}

Remember

  • Never mutate state; build a new object or array and pass it to the setter
  • Objects: { ...obj, field: v }; nested objects need a copy at each level
  • Arrays: add with spread, remove with filter, update with map, sort a copy

Watch out for

  • items.push(x); setItems(items) passes the same array reference, so React may skip the update.

Check yourself

  1. Which update correctly marks the todo with id 7 as done?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: react.dev/learn/updating-objects-in-state