B5 · Part 1 · Beginner
State with useState
State is memory between renders — and setting it is a request, not an assignment.
10On this page
Why a plain variable does not work
function Counter() {
let count = 0; // reset on every render
return <button onClick={() => count++}>{count}</button>;
}
Two separate problems. The variable is recreated each time the function runs, so it never remembers anything; and changing it does not tell React that anything happened, so nothing re-renders. State solves both.
const [count, setCount] = useState(0);
useState returns the current value and a function to request a
new one. React keeps the value outside your function, so it survives across
renders, and calling the setter is what schedules the next render.
A render is a snapshot
This is the idea that makes the rest of React click. Each render has its own
count, fixed for the whole of that render. The value never
changes mid-render.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
console.log(count); // still the old value — always
}
}
setCount does not assign to count. It tells React
"next time, use this", and React re-runs the component with a fresh
count. Inside this call, count is a
const and it is not going to move.
The classic three-clicks bug
setCount(count + 1);
setCount(count + 1);
setCount(count + 1); // count goes up by 1, not 3
All three read the same snapshot. If count is 0, all three say
"make it 1". The fix is the updater form, which receives the latest
value rather than closing over one:
setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1); // 3
Rule of thumb: if the next value is computed from the current one, use the updater form. It is never wrong, and it is required inside timers, intervals and any async callback.
Batching
React collects the state changes made during one event and re-renders once at
the end. Three setState calls in a click handler produce one
render, not three. Since React 18 that also applies inside promises, timeouts
and native event handlers.
How React knows which state is which
useState does not take a name. So how does React tell two
useState calls in the same component apart? By call order.
It keeps a list per component instance and walks it in the same order every
render.
const [name, setName] = useState(""); // slot 0
const [age, setAge] = useState(0); // slot 1
Which is the entire reason for the rules of hooks. Put a hook inside an
if and the call order changes between renders, so slot 1 returns
the value that belonged to slot 0 — your age lands in your name.
if (loggedIn) {
const [x, setX] = useState(0); // ✗ shifts every slot after it
}
The rules of hooks, and why they exist
- Only at the top level. Not in conditions, loops, or nested functions — call order must be identical on every render.
- Only from React functions. Components or other hooks; there is no component instance to attach to otherwise.
Both are lint rules, and both catch real bugs rather than style. If you need conditional state, put the condition inside the component that owns it, or split it into two components.
Several states or one object?
// separate — usually better
const [name, setName] = useState("");
const [age, setAge] = useState(0);
// one object — when the fields always change together
const [form, setForm] = useState({ name: "", age: 0 });
Separate state is simpler to update and lets React skip renders more often. Group fields only when they genuinely move as a unit — a form's values, a coordinate pair — and remember that grouping means every update needs a spread.
State must be replaced, never mutated
React decides whether to re-render by comparing the old value with the new one by reference. Mutating an object gives it the same reference back, so it concludes nothing changed and skips the render.
// ✗ mutation — no re-render
user.name = "Ana"; setUser(user);
items.push(newItem); setItems(items);
items.sort(); setItems(items);
// ✓ new value every time
setUser({ ...user, name: "Ana" });
setItems([...items, newItem]);
setItems([...items].sort());
| Operation | Immutable version |
|---|---|
| add to end | [...items, item] |
| add to front | [item, ...items] |
| remove by id | items.filter((i) => i.id !== id) |
| update one | items.map((i) => (i.id === id ? { ...i, done: true } : i)) |
| insert at index | [...items.slice(0, i), item, ...items.slice(i)] |
| sort / reverse | copy first: [...items].sort() |
map, filter, slice and
concat return new arrays. push, pop,
splice, sort and reverse change the one
you have. Knowing which is which is most of this.
Nested state
The spread is shallow, so a nested update needs a spread at every level it passes through:
setUser({
...user,
address: { ...user.address, city: "Pune" },
});
If you are writing three levels of that, the state is shaped wrong. Flatten
it, or split it into separate useState calls, or move to
useReducer.
Bailing out of a render
If you set state to a value React considers equal — compared with
Object.is — it may skip the re-render entirely.
setCount(5); // count is already 5 → no re-render
setUser({ ...user }); // a new object → always re-renders
This is why spreading an object "just to be safe" is not free: an identical copy is a different reference, so React re-renders even though nothing changed. React may still render once more before bailing out, so do not treat it as a guarantee — treat it as a reason not to churn objects needlessly.
Lazy initial state
The argument to useState is evaluated on every render,
even though it is only used on the first one. If it is expensive, pass a
function instead — React calls it once.
useState(JSON.parse(localStorage.getItem("draft"))); // runs every render
useState(() => JSON.parse(localStorage.getItem("draft"))); // runs once
What should be state, and what should not
The most common cause of tangled React is state that should not exist. Before
adding a useState, check:
| Question | If yes |
|---|---|
| Can it be computed from props or other state? | Compute it during render. Do not store it. |
| Does the UI change when it changes? | If no, it should be a ref. |
| Does another component need it too? | Lift it to the nearest common parent. |
| Is it the same value in two places? | Delete one. Two sources of truth always drift. |
const [items, setItems] = useState([]);
const [count, setCount] = useState(0); // ✗ derived — will drift
const count = items.length; // ✓ always right, free
Interview answer, one sentence
"State is a value React keeps between renders, and each render sees a fixed snapshot of it. Setting state schedules a re-render rather than assigning, which is why you use the updater form when the next value depends on the current one, and why state has to be replaced rather than mutated for React to notice at all."
Opens in the editor: write it, run it, and check it against real tests.
All 46 for this level →Finished reading?
0 of 57 chapters read in this series