Lesson 8 of 31
useState and State Updates
Giving a component its own memory with useState, why state updates trigger a re-render, the functional-update form, and why React batches multiple updates together.
Every component you've seen so far has been static — it renders the same
thing every time. useState is the hook that gives a component its own
memory: a value that persists between renders, and that causes a re-render
whenever it changes.
The basic shape
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}useState(0) does two things: it initializes the state to 0 on the
component's first render, and it returns a pair — the current value
(count) and a function to update it (setCount). Calling setCount
schedules React to re-run this component's function with the new value,
which is what makes the button's text update.
The 0 argument is only used on the very first render. On every render
after that, useState returns whatever was last set, ignoring the initial
value entirely — the same way a variable's initializer only runs once.
State doesn't change mid-render
This is the detail that trips people up first: calling the setter doesn't
change the variable right there in the function. count inside a given
render is a plain value, captured for that render, and it stays that value
for the rest of that render's execution — even immediately after calling
setCount.
function handleClick() {
setCount(count + 1);
console.log(count); // still the OLD value — this render hasn't re-run yet
}The update takes effect the next time the component renders, not synchronously at the call site.
The batching trap, and its fix
Because setCount(count + 1) reads count from the current render's
closure, calling it multiple times in the same event handler doesn't
compound the way you'd intuitively expect:
function handleTripleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
// count only ends up +1, not +3 — all three read the SAME count
}All three calls happen before React re-renders, so all three read the
identical count from this render and all three say "set it to that same
value plus one." The fix is the functional update form — pass a function
instead of a value, and React calls it with whatever the state will actually
be at that point, including earlier updates already queued in the same
batch:
function handleTripleClick() {
setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);
// now correctly +3
}A good habit: reach for the functional form any time the new state depends on the previous state, rather than on some independent value.
Updates are batched by design
React groups multiple setState calls that happen within the same event
handler (and, since React 18, within promises, timeouts, and native event
handlers too — "automatic batching") into a single re-render, rather than
re-rendering after each one individually:
function handleClick() {
setLoading(false);
setError(null);
setData(newData);
// one re-render for all three, not three separate re-renders
}This is a performance win you get for free — three state updates that belong together produce one render pass, not three.
State holding objects and arrays
useState can hold any value, including objects and arrays — but React
decides whether to re-render by checking if the new value is a different
reference from the old one (Object.is comparison), not by deep-comparing
contents. That means state must be replaced with a new object or array, not
mutated in place:
// Wrong — same array reference, may not trigger a re-render
function addItem(item) {
items.push(item);
setItems(items);
}
// Right — a new array reference
function addItem(item) {
setItems([...items, item]);
}This is the same "don't mutate, make a copy" habit from the JavaScript track's arrays-and-objects lesson — in React it's not just good practice, it's the difference between an update working and silently doing nothing.
Try it yourself
What to remember
useStatereturns a current value and a setter; calling the setter schedules a re-render with the new value, it doesn't mutate anything on the spot.- A render's state variable is a fixed snapshot for that render — it doesn't change again until the next render actually happens.
- When new state depends on old state, use the functional update form (
setX(x => ...)) so multiple queued updates compound correctly. - React batches state updates within an event handler into one re-render, and — since React 18 — across promises and timeouts too.
- Never mutate state directly; create a new object or array so React's reference check notices the change.
Check yourself
4 questions · pass 3/4 to unlock Controlled Forms
1.What does calling
setCount(count + 1)actually do?2.Why does this code only ever increment by 1, no matter how many times
increment()is called inside one event handler?function increment() { setCount(count + 1); } function handleClick() { increment(); increment(); increment(); }3.What fixes the previous problem, so calling increment() three times actually adds 3?
4.Why must you write
setItems([...items, newItem])rather thanitems.push(newItem); setItems(items);?
4 left to answer