Skip to content
Laviyx Learn

Try

React course menu

Updating Objects and Arrays in React State (Immutability)

Update objects and arrays in React state correctly: why you must not mutate state, copying objects with spread, nested objects, adding, removing, updating and reordering array items with map, filter and spread, and when to reach for useReducer.

Beginner9 min readUpdated
On this page

State can hold objects and arrays, but there is one rule: never change (mutate) them directly. Always create a new object or array and pass it to the setter.

Why mutation does not work#

React decides whether to re-render by checking whether the new state is a different value. Mutating an object keeps the same object, so React sees "no change":

JSX
import { useState } from "react";

export default function App() {
  const [user, setUser] = useState({ name: "Asha", points: 10 });

  function wrong() {
    user.points = user.points + 5;     // mutates the existing object
    setUser(user);                     // same object, so React skips the update
  }

  function right() {
    setUser({ ...user, points: user.points + 5 });   // a new object
  }

  return (
    <div>
      <p>{user.name} has {user.points} points</p>
      <button onClick={wrong}>+5 (mutate)</button>{" "}
      <button onClick={right}>+5 (new object)</button>
    </div>
  );
}
Result

Click "mutate" a few times: nothing changes on screen, although the hidden object's points are increasing. Then click "new object": the screen jumps to include all the hidden changes plus 5. Mutated state leads to exactly this kind of confusing bug.

Updating objects with spread#

{ ...obj, key: newValue } copies every property and overrides the ones you list:

JSX
import { useState } from "react";

export default function App() {
  const [profile, setProfile] = useState({ name: "Ravi", city: "Chennai", newsletter: false });

  return (
    <div>
      <input value={profile.name} onChange={(e) => setProfile({ ...profile, name: e.target.value })} />
      <select value={profile.city} onChange={(e) => setProfile({ ...profile, city: e.target.value })} style={{ marginLeft: 8 }}>
        <option>Chennai</option>
        <option>Hyderabad</option>
        <option>Kolkata</option>
      </select>
      <label style={{ marginLeft: 8 }}>
        <input
          type="checkbox"
          checked={profile.newsletter}
          onChange={(e) => setProfile({ ...profile, newsletter: e.target.checked })}
        />{" "}
        Newsletter
      </label>
      <pre>{JSON.stringify(profile, null, 2)}</pre>
    </div>
  );
}
Result

Nested objects#

Spread copies only one level. To change a nested property, copy each level on the way down:

JSX
const [order, setOrder] = useState({ id: 7, address: { city: "Pune", pin: "411001" } });

setOrder({
  ...order,
  address: { ...order.address, pin: "411038" },
});

Deep nesting makes updates verbose, which is a hint to keep state flatter where possible.

Arrays: the non-mutating methods#

Avoid methods that change the array (push, pop, splice, sort, reverse, assigning arr[i] = ...). Use ones that return a new array:

GoalUse
Add[...items, newItem] or [newItem, ...items]
Removeitems.filter((item) => item.id !== id)
Change one itemitems.map((item) => item.id === id ? { ...item, done: true } : item)
Insert in the middle[...items.slice(0, i), newItem, ...items.slice(i)]
Sort / reverseitems.toSorted(...), items.toReversed() (or copy first: [...items].sort(...))

All of them in one small shopping list:

JSX
import { useState } from "react";

let nextId = 4;

export default function App() {
  const [items, setItems] = useState([
    { id: 1, name: "Tomatoes", bought: false },
    { id: 2, name: "Onions", bought: true },
    { id: 3, name: "Rice", bought: false },
  ]);
  const [text, setText] = useState("");

  function add() {
    if (!text.trim()) return;
    setItems([...items, { id: nextId++, name: text.trim(), bought: false }]);
    setText("");
  }
  const toggle = (id) => setItems(items.map((it) => (it.id === id ? { ...it, bought: !it.bought } : it)));
  const remove = (id) => setItems(items.filter((it) => it.id !== id));
  const sortByName = () => setItems(items.toSorted((a, b) => a.name.localeCompare(b.name)));

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} placeholder="Add item" />
      <button onClick={add}>Add</button>
      <button onClick={sortByName} style={{ marginLeft: 6 }}>Sort A–Z</button>
      <ul>
        {items.map((it) => (
          <li key={it.id}>
            <label style={{ textDecoration: it.bought ? "line-through" : "none" }}>
              <input type="checkbox" checked={it.bought} onChange={() => toggle(it.id)} /> {it.name}
            </label>
            <button onClick={() => remove(it.id)} style={{ marginLeft: 8 }}>✕</button>
          </li>
        ))}
      </ul>
      <p>{items.filter((it) => !it.bought).length} items left to buy</p>
    </div>
  );
}
Result

Note how "items left to buy" is calculated from items during rendering rather than kept in separate state.

When updates get complicated#

If many event handlers update the same object or array in different ways, move the update logic into one place with useReducer. Libraries such as Immer also let you write "mutating" code that produces new objects behind the scenes.

Common mistakes#

Practice#

Practice exercise

Make a list of three tasks, each with a "Done" button that marks it done (strikes it through) and an "Undo all" button that marks every task as not done. Do not mutate the array.

Show hint

"Done": tasks.map((t) => t.id === id ? { ...t, done: true } : t). "Undo all": tasks.map((t) => ({ ...t, done: false })).

Show solution
JSX
import { useState } from "react";

export default function App() {
  const [tasks, setTasks] = useState([
    { id: 1, text: "Pay electricity bill", done: false },
    { id: 2, text: "Book train ticket", done: false },
    { id: 3, text: "Water plants", done: false },
  ]);
  const markDone = (id) => setTasks(tasks.map((t) => (t.id === id ? { ...t, done: true } : t)));
  const undoAll = () => setTasks(tasks.map((t) => ({ ...t, done: false })));

  return (
    <div>
      {tasks.map((t) => (
        <p key={t.id}>
          <span style={{ textDecoration: t.done ? "line-through" : "none" }}>{t.text}</span>{" "}
          {!t.done && <button onClick={() => markDone(t.id)}>Done</button>}
        </p>
      ))}
      <button onClick={undoAll}>Undo all</button>
    </div>
  );
}
Result

Key takeaway#

Next: Forms and Controlled Inputs.

Finished this lesson?