State Updates Are Asynchronous

A Setter Call Doesn't Change the Variable You're Holding

State Updates Are Asynchronous

Calling `setCount(n + 1)` doesn't change `count` immediately. The new value shows up on the next render.

4 min read Level 2/5 #react#state#async-updates
What you'll learn
  • Understand that state updates queue a re-render
  • Avoid reading state right after setting it
  • Recognize the "stale state" pitfall

A common surprise: setting state does not change the variable in your current render. It queues a re-render with the new value.

The Classic Confusion

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

  function handleClick() {
    setCount(count + 1);
    console.log(count);  // logs the OLD count, not the new one
  }

  return <button onClick={handleClick}>{count}</button>;
}

count is a const captured in this render. setCount(...) tells React “next time this component renders, use the new value”. The new value shows up in the next run of the function.

Snapshots, Not Live References

Every render gets its own snapshot of state. Inside one render, count is a fixed value. Three setter calls don’t stack:

function handleClick() {
  setCount(count + 1);  // count is 0 → schedule 1
  setCount(count + 1);  // count is still 0 → schedule 1
  setCount(count + 1);  // count is still 0 → schedule 1
}
// Final count after click: 1, not 3

All three calls read the same count (the snapshot for this render). To stack, use the updater function form (next lesson).

Reading the Latest Value

You usually don’t need to. If you want to do something after a state change, do it on the next render with useEffect — covered in chapter 4.

Setting Objects and Arrays

Don’t mutate. Always replace:

const [user, setUser] = useState({ name: "", age: 0 });

// ✗ Mutating — React doesn't notice, no re-render
user.name = "Ada";
setUser(user);

// ✓ Replace with a new object
setUser({ ...user, name: "Ada" });

Same for arrays — setItems([...items, newItem]), not items.push(newItem).

The “Set, Then Read” Anti-Pattern

// ✗ Doesn't work the way you'd hope
function handleAdd() {
  setItems([...items, newItem]);
  console.log(items.length);   // OLD length
  send(items);                  // OLD items
}

Either compute the value once and use it twice:

function handleAdd() {
  const nextItems = [...items, newItem];
  setItems(nextItems);
  send(nextItems);
}

Or move the dependent work to an effect.

Up Next

When the new state depends on the previous state, use the updater function form.

Updater Functions →