useState in Depth

Reviewed & published by Brayan K

useState stores a value that React remembers between renders, and gives you a setter to change it. In this lesson you'll go deeper: why setting state is asynchronous, why React batches multiple updates into one re-render, and why the functional updater form setX(prev => ...) is the only correct way to update state from its previous value.

Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

1️⃣ State Updates Are Asynchronous

When you call setCount, React does not change the count variable on the spot. It schedules a re-render. During the current render, count is a fixed, captured value (a closure). So calling setCount(count + 1) three times reads the same old count all three times — they all compute the same next value.

function Counter() {
  const [count, setCount] = useState(0);
  function handleTriple() {
    setCount(count + 1);   // reads 0 -> 1
    setCount(count + 1);   // reads 0 -> 1  (count is still 0!)
    setCount(count + 1);   // reads 0 -> 1
  }                        // result: 1, not 3
  return <button onClick={handleTriple}>{count}</button>;
}
// Simulating what React sees when you call the setter 3 times in one event.
// During THIS render, "count" is a fixed value captured by the closure.
let count = 0;   // pretend this is the current render's state value

// You call setCount(count + 1) three times in a click handler:
const update1 = count + 1;   // reads 0 -> 1
const update2 = count + 1;   // reads 0 -> 1  (count is STILL 0 here!)
const update3 = count + 1;   // reads 0 -> 1

console.log(update1, update2, update3);   // 1 1 1
// React batches them: last write wins, so the screen shows 1, not 3.
console.log("Final count on screen:", update3);   // 1

// Lesson: when the next value depends on the previous, don't read "count".

2️⃣ The Functional Updater Form

When the next value depends on the previous one, pass a function to the setter: setCount(prev => prev + 1). React calls it with the latest pending value, so chained calls stack correctly — even under batching.

function handleTriple() {
  setCount(prev => prev + 1);   // 0 -> 1
  setCount(prev => prev + 1);   // 1 -> 2
  setCount(prev => prev + 1);   // 2 -> 3  ✅ now it reaches 3
}
// The FUNCTIONAL UPDATER form: setCount(prev => prev + 1)
// React calls your function with the latest PENDING value each time,
// so three calls correctly stack up.

let pending = 0;   // React's running pending value

const inc = prev => prev + 1;   // what you pass to setCount

pending = inc(pending);   // 0 -> 1
pending = inc(pending);   // 1 -> 2
pending = inc(pending);   // 2 -> 3

console.log("Final count on screen:", pending);   // 3
// Each call builds on the previous result — correct even when batched.

Now your turn — write the updater for a toggle. It should return the opposite of its argument:

// 🎯 YOUR TURN — fill in each ___ then press "Try it Yourself".
// Make a toggle that flips between true and false correctly,
// even if called twice in a row. Use the functional updater form.

let value = false;

// 1) Write an updater that returns the OPPOSITE of its argument.
const toggle = prev => ___;        // 👉 e.g. !prev

value = toggle(value);   // false -> true
value = toggle(value);   // true  -> false
value = toggle(value);   // false -> true

console.log(value);

// ✅ Expected output:
//    true

3️⃣ Batching — One Re-render for Many Updates

For performance, React batches all the state updates that happen in a single event and re-renders once at the end. Set three pieces of state in one click handler and your component still renders only one extra time. Since React 18, this automatic batching also covers updates inside setTimeout, promises, and native handlers.

// BATCHING: multiple setState calls in one event = ONE re-render.
// Here we count how many times the component would re-render.

let renders = 0;
function render() { renders++; }

// Inside a single click handler you update three pieces of state:
function handleClick() {
  // setName(...), setAge(...), setActive(...) all queued together
  // React flushes them once, then renders a single time:
  render();   // <- only ONE render for all three updates
}

handleClick();
console.log("Re-renders after 3 setState calls:", renders);   // 1
// Without batching it would be 3. Batching keeps the UI fast.

🧩 Reorder Challenge

These lines build a correct counter component, but they're scrambled. Put them in the right order:

A.   return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
B.   function Counter() {
C.   const [count, setCount] = useState(0);
D.   }

Common Errors (and the fix)

📋 Quick Reference

NeedForm
Set to a fixed valuesetX(5)
Next depends on previoussetX(prev => prev + 1)
Toggle a booleansetOpen(o => !o)
Append to arraysetArr(a => [...a, item])
Update object fieldsetU(u => ({ ...u, age: 30 }))

🧠 Quick Recall

1. count is 0. You call setCount(count + 1) twice in one handler. What shows on screen?

1. Both reads see the old 0, compute 1, and batching keeps the last write — so 1.

2. Same start, but you call setCount(c => c + 1) twice. Now what?

2. Each updater builds on the pending value: 0→1→2. The screen shows 2.

3. You set three different state variables in one click. How many extra re-renders?

One. React batches all three into a single re-render.

Mini-Challenge: A Like Counter

Simulate five "like" clicks using the functional updater form. Run it and confirm the total is 5.

// 🎯 MINI-CHALLENGE: a "like" counter that survives batching
//
// 1. Start with likes = 0.
// 2. Simulate clicking "like" 5 times using the functional updater
//    form: likes = bump(likes), where bump(prev) => prev + 1.
// 3. console.log the final number of likes.
//
// ✅ Expected output:
//    5

let likes = 0;

// your code here

🎉 Lesson Complete

Practice quiz

Are state updates synchronous?

  • Yes, the variable changes immediately
  • Only in production
  • No — setState schedules a re-render; the current variable does not change immediately
  • Only for objects

Answer: No — setState schedules a re-render; the current variable does not change immediately. setState schedules an update. During the current render the state variable stays the same; React re-renders with the new value afterwards.

count is 0. You call setCount(count + 1) three times in one handler. What shows on screen?

  • 1
  • 3
  • 0
  • 2

Answer: 1. Each call reads the same stale count (0) from the current render's closure, so all three compute 1. Batching keeps the last write: the screen shows 1.

How do you correctly increment based on the previous value?

  • setCount(count + 1)
  • count = count + 1
  • setCount(count++)
  • setCount(prev => prev + 1)

Answer: setCount(prev => prev + 1). Use the functional updater setCount(prev => prev + 1). React calls it with the latest pending value, so chained calls stack correctly even when batched.

Starting at 0, you call setCount(c => c + 1) twice in one handler. What shows?

  • 0
  • 2
  • 1
  • 3

Answer: 2. Each updater builds on the latest pending value: 0 -> 1 -> 2. The screen shows 2.

What is batching in React?

  • Grouping multiple state updates in one event into a single re-render
  • Running effects in parallel
  • Caching component output
  • Delaying renders by one second

Answer: Grouping multiple state updates in one event into a single re-render. React batches the state updates that happen in a single event and re-renders once at the end for performance.

You set three different state variables in one click. How many extra re-renders?

  • Three
  • Two
  • One
  • Zero

Answer: One. React batches all three updates into a single re-render, so there is just one extra render.

In React 18+, automatic batching also covers updates inside which contexts?

  • Only synchronous event handlers
  • setTimeout, promises, and native event handlers too
  • Only class components
  • Only the initial render

Answer: setTimeout, promises, and native event handlers too. Since React 18, automatic batching extends to updates inside timeouts, promises, and native handlers — not just React event handlers.

When must you use the functional updater form setX(prev => ...)?

  • Never; the value form is always fine
  • Only for strings
  • Only on the first render
  • Any time the next state depends on the previous state

Answer: Any time the next state depends on the previous state. Whenever the next state depends on the previous (counters, toggles, appending to arrays), the updater form is the only correct choice under batching/async updates.

Which correctly appends an item to an array in state without mutating?

  • arr.push(x); setArr(arr)
  • setArr(a => [...a, x])
  • setArr(arr.push(x))
  • arr[arr.length] = x; setArr(arr)

Answer: setArr(a => [...a, x]). Create a new array: setArr(a => [...a, x]). Mutating the existing array with push keeps the same reference and skips the re-render.

What is the benefit of passing a function to useState, e.g. useState(() => expensiveInit())?

  • It re-runs expensiveInit on every render
  • It makes the state asynchronous
  • It runs expensiveInit only once, on the initial render (lazy initialization)
  • It memoizes the setter

Answer: It runs expensiveInit only once, on the initial render (lazy initialization). Lazy initialization: passing a function makes React call it only on the first render to compute the initial state, avoiding expensive work on every render.

Continue this course

Frequently asked questions

Why does my count only go up by 1 when I call setCount(count + 1) three times?

Each call reads the SAME stale count from this render (a closure capture). All three compute the same next value, so React batches them into one update. Use the updater form setCount(c => c + 1) so each call builds on the previous pending value.

Are state updates synchronous?

No. setState schedules an update; the variable in the current render does not change immediately. React re-renders with the new value afterwards. Reading the state variable right after calling the setter still shows the old value.

What is batching?

React groups multiple state updates that happen in the same event into a single re-render for performance. In React 18+ this also covers updates inside promises, timeouts, and native event handlers (automatic batching).

When must I use the functional updater form?

Any time the next state depends on the previous state — counters, toggles, appending to arrays — use setX(prev => ...). It is the only form that is correct when updates are batched or asynchronous.