What you'll learn
Quick Answer
useState returns the current value and a setter. Calling the setter schedules a re-render rather than changing the variable immediately, so reading the state right after setting it gives the old value. Use the functional form when the new value depends on the previous one, and always create a new object or array rather than mutating, because React compares by reference to decide whether to re-render.
The Basics, and the First Surprise
const [count, setCount] = useState(0);Array destructuring gives you the current value and a function to change it. The argument is the initial value, used only on the first render.
The first thing that surprises people:
function handleClick() {
setCount(count + 1);
console.log(count); // logs the OLD value, not the new one
}count is a constant within this render. Calling the setter does not reassign it — it tells React to render the component again, and on that next render count will be a new constant with the new value.
So state is not a variable you mutate. It is a value React hands you per render, plus a way to request the next one.
The consequence people hit next:
setCount(count + 1);
setCount(count + 1); // total increase: 1, not 2Both calls read the same count from this render. Both compute the same result. React batches them and the second simply overwrites the first.
The Functional Updater
When the new value depends on the previous one, pass a function instead of a value. React calls it with the latest state, including updates queued in the same batch.
setCount(c => c + 1);
setCount(c => c + 1); // now increases by 2Use it whenever you are deriving from the current state — incrementing, toggling, appending.
setIsOpen(open => !open); // toggle
setItems(prev => [...prev, newItem]); // appendIt also solves the stale closure problem, which is subtler. A callback created in one render captures that render's values forever:
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // count is always 0 here — captured on first render
}, 1000);
return () => clearInterval(id);
}, []); // empty deps: the effect never sees a newer countThe counter goes 0, 1, 1, 1… The functional form fixes it without touching the dependency array, because it does not read count at all:
setCount(c => c + 1); // always operates on the latest valueThis pattern — intervals, subscriptions, event listeners registered once — is where the functional updater stops being a style preference and becomes the only correct option.
Updating Objects and Arrays
React decides whether to re-render by comparing the new state to the old by reference. Mutating an object keeps the same reference, so React sees no change.
// Nothing happens — same array, same reference
items.push(newItem);
setItems(items);
// Correct — a new array
setItems([...items, newItem]);The patterns worth memorising:
// Add
setItems([...items, newItem]);
// Remove
setItems(items.filter(i => i.id !== id));
// Update one item
setItems(items.map(i => i.id === id ? { ...i, done: true } : i));
// Update one field of an object
setUser({ ...user, name: 'Riya' });
// Update a nested field — spread at every level you change
setUser({
...user,
address: { ...user.address, city: 'Pune' }
});That last one is the trap. Spread is shallow, so { ...user } shares the same address object. Mutating it changes both copies and React still sees no change at the top level.
Also note which array methods mutate: push, pop, splice, sort and reverse all modify in place. Copy first — [...items].sort() — or use the non-mutating toSorted where available.
If nested updates become painful, that is usually a signal to flatten the state shape rather than to write deeper spreads.
Structuring State Well
Do not store what you can compute. This is the most valuable habit here.
// Two sources of truth that can drift apart
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
// One source of truth — total cannot be wrong
const [items, setItems] = useState([]);
const total = items.reduce((sum, i) => sum + i.price, 0);Every derived value kept in state is a value that can become inconsistent. Filtered lists, counts, totals and "is the form valid" should almost always be computed during render.
Group state that changes together, split state that does not. A form with five fields updated independently is fine as five useState calls, or as one object — but if you use one object, remember to spread the rest:
setForm({ ...form, email: value }); // forgetting the spread loses the other fieldsUse lazy initial state for expensive setup. The argument to useState is evaluated on every render, even though only the first value is used:
useState(expensiveCompute()); // runs every render, result discarded
useState(() => expensiveCompute()); // runs onceThis matters when reading from localStorage or parsing something large.
State is per component instance. Two instances of the same component have completely independent state — which is obvious once stated but a common early confusion.
Problems and Their Causes
"My component does not re-render." You mutated instead of replacing. Check for push, direct property assignment, or a nested object shared through a shallow spread.
"State resets unexpectedly." The component is unmounting and remounting. Usually the parent is rendering it under a different key, or the component is defined inside another component so it is a new type on every render:
function Parent() {
function Child() { ... } // new function identity each render — state lost
return <Child />;
}Define components at module level.
"Too many re-renders." You called the setter during render rather than in a handler or effect:
function Bad() {
const [n, setN] = useState(0);
setN(n + 1); // render → update → render → ...
return <p>{n}</p>;
}
<button onClick={setCount(1)}> // calls immediately during render
<button onClick={() => setCount(1)}> // correct — passes a function"An input will not type." A controlled input with value but no onChange is locked to the state value. Add the handler, or use defaultValue for an uncontrolled input.
"Props changed but state did not." The initial value is only used on the first render. Deriving state from props is usually a design smell — compute during render, or reset with a key prop when the identity genuinely changes.
