[JS] Update State Based on Old State in React

[JS] Update State Based on Old State in React

Hello everyone, お元気ですか?

Today I want to talk about one of those sneaky little bugs that looks fine... until it doesn’t: updating state based on its previous value in React.

What You Can Expect from This Post

When I first started using useState, I thought updating state was as simple as calling setState(newValue). And for most cases, it is.

But then I ran into this problem — where my state didn't behave the way I expected. Sometimes it seemed to skip updates, or the value was wrong. That’s when I learned about the right way to update state based on its previous value.


The Wrong Way (That Looks Fine... Until It Isn’t)

Let’s say you're building a simple counter, and you want to increment the number by 1 when a button is clicked:


const [count, setCount] = useState(0);

const handleClick = () => {
  setCount(count + 1);
};

At first glance, this looks totally fine. But what happens if this gets called multiple times quickly, or inside something asynchronous like setTimeout, or even in a function where count is already outdated like below example?

const [count, setCount] = useState(0);

const handleClick = () => {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
};

Normally we expect count to be 3; however it is very likely count here might be referring to an old value of count. And you'll end up with missed updates.


The Right Way: Use the Callback Form

React actually gives you a safer way to update state — by passing a callback to setCount, which receives the previous state as a parameter by default:

setCount(prevCount => prevCount + 1);

This way, no matter how many times or where you call it, you're always working with the latest state value.

Here’s the updated example:

const [count, setCount] = useState(0);

const handleClick = () => {
  setCount(prevCount => prevCount + 1);
  setCount(prevCount => prevCount + 1);
  setCount(prevCount => prevCount + 1);
};

Now even if you call handleClick ten times in a row, you’ll get the right result which is 3.


What’s Happening Behind the Scenes?

So why does this happen? Why isn’t setCount(count + 1) always reliable?

The reason lies in how React handles state updates under the hood.

React batches state updates for performance reasons — especially in event handlers and lifecycle methods. This means when you call setState(...), it doesn't immediately update the state. Instead, React schedules that update to be processed later, during the reconciliation phase.

Here’s the tricky part: If you read from count before React has finished updating it, you’re still working with the stale value — the version of count before the last setCount(...) was processed.

Now imagine you do this:

setCount(count + 1);
setCount(count + 1);

You might expect count to increment by 2.

But if count is still 0 when these two lines run, they both evaluate to 1 — so the final count is just 1, not 2.

By using the callback form:

setCount(prev => prev + 1);
setCount(prev => prev + 1);

React knows that you want to update based on the most recent value — and it’ll pass in the latest one available, even if multiple updates are batched together. This ensures that your updates are sequential, safe, and correct, even in complex scenarios.


Conclusion

Next time when you want to update state based on previous state, use callback form instead!

じゃ、またね!