State Updates in React

Setting state does not change the current render. It asks React to calculate a later render, and React processes queued updates in a predictable order.

State update queues

Every render receives a snapshot of state. Calling a setter such as setScore queues an update, but the score variable in the running event handler remains the value from that render.

The log still prints 0 because this handler closes over the current render's snapshot.
import { useState } from "react";

function Scoreboard() {
  const [score, setScore] = useState(0);

  function handleClick() {
    setScore(score + 1);
    console.log(score); // 0, not 1
  }

  return <button onClick={handleClick}>
    Score: {score}
  </button>;
}

A setter is a request, not an assignment. If later code in the same handler needs the next value, calculate it in a local variable or use a functional update when it depends on queued state.

Batching groups updates

React waits until the event handler has finished before processing its state updates. This batching avoids an extra render after every setter call and prevents the screen from showing half-finished state.

Both setters are handled together, so the component normally renders once after the click.
function CheckoutButton() {
  const [status, setStatus] = useState("idle");
  const [message, setMessage] = useState("");

  function handleCheckout() {
    setStatus("submitting");
    setMessage("Processing order");
  }

  return (
    <button onClick={handleCheckout}>
      {status === "idle" ? "Checkout" : message}
    </button>
  );
}

React does not batch separate intentional interactions into one event. A second click gets its own handler and its own state snapshot. Modern React also batches many updates from promises, timers, and native event handlers when the app uses createRoot.

Functional updates use pending state

When the next value depends on the previous value, pass an updater function to the setter. React puts that function in the queue. During the next render, it calls each updater in order and passes the result to the next one.

Repeats one replacement

Three calls to setCount(count + 1) all calculate from the same snapshot, so they each request the same value.

Builds on the queue

Three calls to setCount(c => c + 1) receive 0, then 1, then 2, producing 3.

This button adds three because every updater receives the latest pending value.
function Counter() {
  const [count, setCount] = useState(0);

  function addThree() {
    setCount(current => current + 1);
    setCount(current => current + 1);
    setCount(current => current + 1);
  }

  return <button onClick={addThree}>Count: {count}</button>;
}

Updater functions run while React renders, so keep them pure. Return the next state without changing existing objects, arrays, variables outside the function, or the DOM. In development, Strict Mode may call an updater twice to reveal accidental mutation.

Immutable object updates

Treat objects stored in state as read-only. Mutating an existing object keeps the same reference and changes data that older render snapshots may still rely on. Instead, create a new object and copy the fields that should remain.

Object spread copies the profile, while the later city property replaces only that field.
function ProfileEditor() {
  const [profile, setProfile] = useState({
    name: "Mira",
    city: "Pune",
  });

  function handleCityChange(event) {
    setProfile(current => ({
      ...current,
      city: event.target.value,
    }));
  }

  return (
    <input value={profile.city} onChange={handleCityChange} />
  );
}

Spread syntax is shallow. To change a nested value, copy every object from the changed field back to the top-level state object.

Both settings and its nested notifications object receive new references.
setSettings(current => ({
  ...current,
  notifications: {
    ...current.notifications,
    email: false,
  },
}));

Do not write profile.city = value. Build a new object and pass it to the setter. When state becomes deeply nested and awkward to copy, consider flattening its shape or using a reducer.

Immutable array updates

Arrays in state are objects too, so treat them as read-only. Create a new array with spread syntax, map(), or filter(), then store that array.

  • Add with spread.

    Use [...items, newItem] instead of items.push(newItem).

  • Remove with filter().

    Keep every item whose ID is not the one being removed.

  • Replace with map().

    Return an updated copy for the matching item and return every other item unchanged.

A small task list shows add, toggle, and remove without mutating state.
function TaskList() {
  const [tasks, setTasks] = useState([
    { id: 1, title: "Read the state notes", done: false },
  ]);

  function addTask(task) {
    setTasks(current => [...current, task]);
  }

  function toggleTask(taskId) {
    setTasks(current =>
      current.map(task =>
        task.id === taskId ? { ...task, done: !task.done } : task
      )
    );
  }

  function removeTask(taskId) {
    setTasks(current => current.filter(task => task.id !== taskId));
  }

  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <button onClick={() => toggleTask(task.id)}>
            {task.done ? "Undo" : "Done"}
          </button>
          {task.title}
          <button onClick={() => removeTask(task.id)}>
            Remove
          </button>
        </li>
      ))}
    </ul>
  );
}

Methods such as push(), pop(), splice(), sort(), and reverse() mutate the original array. For sorting or reversing, copy first: const next = [...items]; next.sort(compareItems); setItems(next);.

Check your understanding

  1. Why does reading state immediately after its setter return the old value?
    Show answer

    The running handler belongs to the current render and sees that render's fixed state snapshot. The setter queues work for a later render rather than changing the local variable.

  2. When should you prefer setCount(current => current + 1)?
    Show answer

    Use a functional update when the next state depends on previous or already queued state. React passes each updater the latest pending value.

  3. How do you update one object inside an array without mutation?
    Show answer

    Use map() to create a new array. For the matching item, return a new object with the changed field; return every other item unchanged.