Skip to content

React Tutorial State and Events

Updating State Correctly: Functional Updates and Batching

Lesson 45 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.

Calling the setter is simple, but a few details decide whether your updates behave. This lesson covers the ones that trip up beginners.

Setting does not change the variable you already have

When you call setCount(5), React schedules a re-render. The count variable in the function that is already running does not change. The new value appears in the next render.

App.jsx
JSX
function App() {
  const [count, setCount] = React.useState(0);

  function addThree() {
    setCount(count + 1); // count is 0 here, so this asks for 1
    setCount(count + 1); // still 0, so asks for 1 again
    setCount(count + 1); // still asks for 1
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={addThree}>Add three</button>
    </div>
  );
}

After clicking, the count is 1, not 3. All three calls used the same count (0), so they all asked for 1. The next lesson, State as a Snapshot, explains why.

The updater function

When the new value depends on the previous one, pass a function. React calls it with the latest pending value:

App.jsx
JSX
function App() {
  const [count, setCount] = React.useState(0);

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

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={addThree}>Add three</button>
    </div>
  );
}

Now it adds 3. React queues the updater functions and applies them one after another, each receiving the result of the previous one. The convention is to name the parameter after the state (c for count, or prev).

Rule of thumb: if the next value is computed from the previous value, use the updater form. If it is a fixed value (setName("Ada")) or comes from an event (setText(e.target.value)), pass the value directly.

Updates are batched

React groups all the state updates from one event handler and re-renders once at the end, not after each call. This is called batching, and since React 18 it applies everywhere (event handlers, timeouts, promises). It makes the UI faster and avoids showing half-finished states.

App.jsx
JSX
function App() {
  const [a, setA] = React.useState(0);
  const [b, setB] = React.useState(0);
  const [c, setC] = React.useState(0);
  const renders = React.useRef(0);
  renders.current += 1;

  function update() {
    setA(a + 1);
    setB(b + 1);
    setC(c + 1);
  }

  return (
    <div>
      <p>a={a} b={b} c={c}</p>
      <p>Renders: {renders.current}</p>
      <button onClick={update}>Update three things</button>
    </div>
  );
}

Three setters, but only one extra render: the counter goes from 1 to 2, not to 4. (The render counter uses a ref, which is explained in useRef.)

Setting the same value does nothing

If you set state to a value that is the same as the current one (compared with Object.is), React skips the re-render. That is why mutating an object and setting it again does not work: it is still the same object. See Avoiding State Mutation.

State is replaced, not merged

With useState, the setter replaces the whole value. For objects this differs from how classes worked:

App.jsx
JSX
function App() {
  const [user, setUser] = React.useState({ name: "Ada", age: 36 });

  function birthday() {
    // Wrong: setUser({ age: user.age + 1 }) would lose the name
    setUser({ ...user, age: user.age + 1 });
  }

  return (
    <div>
      <p>{user.name}, {user.age}</p>
      <button onClick={birthday}>Birthday</button>
    </div>
  );
}

We copy the old fields with ...user and override age. Updating Objects in State goes further.

Updater functions must be pure

An updater function should only calculate the next value from the previous one. Do not call other setters or send requests inside it. React may call it twice in development to check this.

Key points

  • Setting state schedules a render; the current variable keeps its old value.
  • Use setX(prev => ...) when the new value depends on the old one.
  • React batches updates into one render, and setters replace rather than merge.