Functional State Updates

⭐ Interview Importance: MEDIUM
⏱️ Revision Time: 2 min

Functional State Updates

As discussed in the “State Updates” topic, state acts as a snapshot. If you try to update the state multiple times in a single event handler based on the previous state, you will encounter the stale state closure problem:

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

function handleClick() {
  setCount(count + 1); // setCount(0 + 1)
  setCount(count + 1); // setCount(0 + 1)
  setCount(count + 1); // setCount(0 + 1)
}
// Result: count is 1

The Solution: Updater Functions

To solve this, you can pass an updater function to your state setter instead of the next state value. React will pass the pending state to this function, allowing you to calculate the next state based on the most up-to-date value in the queue.

function handleClick() {
  setCount(prevCount => prevCount + 1); // prevCount is 0, returns 1
  setCount(prevCount => prevCount + 1); // prevCount is 1, returns 2
  setCount(prevCount => prevCount + 1); // prevCount is 2, returns 3
}
// Result: count is 3

How React processes the queue

When you pass an updater function (e.g., prevCount => prevCount + 1), React queues that function to be executed during the next render phase.

  1. React adds the first function to a queue.
  2. React adds the second function to the queue.
  3. React adds the third function to the queue.
  4. During the next render, React goes through the queue in order, passing the result of the previous function as the argument to the next function.

When to use functional updates

You should always use functional state updates when your new state depends on the previous state.

  • Good: setCount(prev => prev + 1)
  • Bad: setCount(count + 1)

You should only pass a raw value when the new state does not depend on the previous state.

  • Good: setName('Bob')

Interview Questions

Q: Why should you use functional state updates (e.g. setCount(prev => prev + 1)) instead of passing the value directly (setCount(count + 1))?
A: If multiple state updates happen rapidly in a single render cycle, or if the update happens inside an asynchronous callback (like a setTimeout), the closure over the count variable may be stale. Functional updates guarantee you are calculating the new state based on the most recent, committed state.