All posts
ReactReact statesetStateuseStatebatching

Why Calling setCount Three Times Doesn't Add Three in React

React batches state updates and uses snapshots inside event handlers. Learn why setCount(count + 1) three times yields 1, and how functional updaters fix it.

August 10, 2026Siddhartha Mishra

If you have been working with React, then you might already know the answer to this:

javascript
1function App() {
2  const [count, setCount] = useState(0);
3
4  function handleClick() {
5    setCount(count + 1);
6    setCount(count + 1);
7    setCount(count + 1);
8  }
9
10  return (
11    <div>
12      <button onClick={handleClick}>Click me</button>
13      <p>{count}</p>
14    </div>
15  );
16}

Try to guess the value of count when the user clicks the button once.

If you are new to React or JavaScript, or if you came from another language (Java, Python, C++, etc.), your first answer is probably 3. In almost every other programming language that would be correct. But React works differently. To understand why, we need to look a little under the hood.

What actually happens when you call setCount

When the user clicks the button, React does not update the state immediately. Instead, it schedules an update and pushes it into a queue. The crucial detail is that when React queues the update, it takes a snapshot of the current state at that moment. In our example the snapshot is 0.

React batches all state updates that happen inside the same event handler. So the three calls above are not executed one after another right away. They get collected and the queue ends up looking roughly like this:

React state update queue showing three setCount calls from the same snapshot
React state update queue showing three setCount calls from the same snapshot

javascript
setCount(0 + 1);
setCount(0 + 1);
setCount(0 + 1);

Every single update is calculated from the same original snapshot (0). When React later processes the queue during the render phase, it applies them one by one, but each calculation still starts from 0. That is why after one click the count becomes 1, not 3.

This behavior is intentional. By batching updates, React can avoid re-rendering the component three separate times. It collects everything, calculates the final state once, and then re-renders only once. That is a big performance win, especially in larger components.

A simplified look at the phases

Under the hood the process looks something like this:

  1. Event happens — the click handler runs.
  2. Schedule update — React pushes each setCount into a queue and captures the current state snapshot.
  3. Render phase — React processes the queue, calculates the next state, and decides what needs to re-render.
  4. Commit phase — React actually updates the DOM with the new state.

React render and commit phases after batched state updates
React render and commit phases after batched state updates

The key takeaway is that the state value you see inside the event handler is the value from the beginning of that event, not the value after previous setCount calls in the same handler.

How to fix it — use the functional updater

In most cases you should avoid updating the same piece of state multiple times in one event. But sometimes you genuinely need to. The correct way is to pass an updater function instead of a direct value:

javascript
setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);

Now the count correctly becomes 3.

Why does this work?

When you pass a normal value (or an expression that uses count), React stores the already-calculated number in the queue. That number is based on the snapshot. When you pass a function, React stores the function itself. Later, while processing the queue, React calls each function and always gives it the most recent state value. So the second updater receives the result of the first one, the third receives the result of the second, and so on.

This is the recommended pattern whenever your new state depends on the previous state.

The important pattern to remember

There is a clear rule:

  • Pass a value or an expression that uses the current state variable → React uses the snapshot from the moment the event started and simply replaces the state with that value.
  • Pass an updater function (prev => ...) → React runs the function later and always works with the latest state.

Let’s look at a mixed example so this really sticks:

javascript
setCount(count + 10); // uses snapshot
setCount((c) => c + 1); // functional updater
setCount(count + 1); // uses snapshot again

What do you think the final count will be after one click? Many people say 12. The real answer is 1. Here’s why:

  • count + 10 → snapshot is 0 → queues 10
  • c => c + 1 → takes the previous result (10) → becomes 11
  • count + 1 → again uses the original snapshot (0) → queues 1

The last update overwrites everything that came before it. Final value: 1.

React 18 automatic batching

In React 17 and earlier, batching only happened inside React event handlers (like onClick). Updates inside setTimeout, promises, or native event listeners were not batched. React 18 changed this — almost all updates are now batched automatically, even in timeouts and promises. This makes the behavior more consistent, but it also means the “stale snapshot” problem can appear in more places than before.

When you should always use the functional form

  • When the new state depends on the previous state (count + 1, count * 2, toggling a boolean, etc.)
  • When you have multiple updates in the same function
  • When the update happens inside a loop or after an asynchronous operation
  • When you are not 100% sure the current count variable is still fresh

A quick tip for debugging

If you ever see a state value that “should have” updated but didn’t, the first thing to check is whether you used a direct value instead of an updater function. Adding a simple console.log inside the updater can also help you see exactly what value React is working with:

javascript
1setCount((c) => {
2  console.log("Previous value:", c);
3  return c + 1;
4});

One more common case — updating objects or arrays

The same rule applies when your state is an object or array. Always prefer the functional form if the new value depends on the old one:

javascript
setUser((prev) => ({ ...prev, name: "New Name" }));
setItems((prev) => [...prev, newItem]);

This protects you from accidentally overwriting other properties or losing previous items because of a stale snapshot.

React’s batching and snapshot behavior is one of those things that feels strange at first, especially if you come from other languages. Once you understand that:

  1. Updates inside an event are collected into a queue,
  2. Direct values use the state from the beginning of the event, and
  3. Updater functions always receive the latest state,

…the behavior stops being mysterious and becomes predictable.

Use the functional updater whenever the next state depends on the previous one, and you will avoid a whole class of subtle bugs.

Thanks for reading. If this helped, feel free to share it with someone who might need it.