How does useState work? Explain functional updates and lazy initialization.
Take a moment.
Say your answer out loud or write it down first — retrieval is what makes it stick. Reveal when you're ready.
Short answer
useState returns [value, setter]. Functional updates (setX(prev => next)) are safe against stale closures and batching; lazy initialization — passing a function to useState — runs the expensive initial computation only once, on first render.
Detailed explanation
The setter schedules a re-render with a new value; React may batch multiple updates into one render pass. If your new state depends on the previous value — counters, toggles, array appends — use the functional form: setCount(c => c + 1). Calling setCount(count + 1) twice in one handler yields the same number both times because count is frozen from that render's closure; two functional updates compose correctly. Lazy initialization matters when computing initial state is expensive (reading localStorage, building big structures): useState(() => heavySetup()) invokes the function once during the first render only; useState(heavySetup()) would run on *every* render even though the result is ignored after the first. State is preserved across re-renders by position in the hook call order — which is why hooks can't be conditional.
Official docs:React docs: useState
Example
// Stale closure trap:
const [count, setCount] = useState(0);
const broken = () => { setCount(count + 1); setCount(count + 1); }; // +1 total
const fixed = () => { setCount(c => c + 1); setCount(c => c + 1); }; // +2
// Lazy init:
const [filters] = useState(() => JSON.parse(localStorage.getItem('f') ?? '{}'));Interview tip
Demonstrate the double-increment bug live — it proves understanding of closures plus batching. Then mention lazy init for localStorage reads.
Common mistakes
- Calling the setter with computed values inside loops/handlers instead of functional updates.
- Passing an invoked function to useState instead of a thunk.