skip to content

useState

You will learn the state hook end to end: the updater function, why the functional form exists, how the lazy initializer avoids rebuilding expensive initial values, and the Object.is bail-out that silently skips a re-render. Interviewers love the "three setCount(count + 1) calls in a row" question because it exposes whether you see state as a per-render snapshot.

part ofReactoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In React, what does the `useState` hook return, and why does assigning to the destructured state variable directly (for example `count = count + 1` inside a click handler) fail to update the UI?

level: juniorimportance: must knowfreq 85%

answer

  1. it hands back a pair
  2. the value is just a local binding
  3. assignment tells React nothing
  4. the setter is the only channel
  5. fresh binding on every render

basics

~20 s

useState returns a two-item array: the state value for the render that is currently running, and a setter function. That value is an ordinary local binding, so assigning to it tells React nothing — only calling the setter schedules a re-render.

solid answer

~50 s

`useState(initial)` returns exactly two things, which we normally destructure as `const [count, setCount] = useState(0)`: the state value as of the render that is executing, and a setter that asks React to store a new value and re-render. The value is a plain JavaScript binding local to that one call of the component function — React hands it over and then has no further link to it. So `count = count + 1` either throws, because the customary `const` makes the binding read-only, or, if you wrote `let`, quietly updates a variable nothing will read again. Either way React was never told anything changed, so nothing re-renders. `setCount(count + 1)` is the only channel: React stores the value against that component instance and schedules a render, and on the next render `useState` returns the new value into a fresh `count` binding.

go deeper

for a junior

Be able to say out loud that useState gives you a value plus a setter, and that only the setter causes a re-render. Show the destructuring and use the setter, never an assignment.

for a middle

Explain the snapshot mechanic: the binding is recreated on each render and captured by every closure made during it, so writing to it is invisible to React and reading it after a set gives the old value.

for a senior

Show where this bites in real code — handlers, timers and effect callbacks holding a snapshot from an old render — and how you spot the symptom of state that visibly lags one step behind the UI.

for a principal

Frame it as the cost of the snapshot model: React trades live mutable references for predictable per-render values, and your conventions (immutable updates, no ad-hoc instance variables) should protect that trade across the codebase.

## The return value is a pair `useState` takes one argument — the initial state — and returns an array with exactly two entries. Array destructuring is the convention because it lets you name both halves whatever you like: ```jsx function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>; } ``` The first entry is the state value **as of the render that is currently running**. The second is a setter function that asks React to store a new value for this component and schedule a re-render. There is no third element, no object with named fields, and no way to read current state back out of the setter. ## The value is a snapshot, not a live channel The most common mental-model error is imagining `count` as a live view onto a box React owns, so that writing to it writes into the box. It is not. Each time React renders your component it calls the function again, and on each call `useState` returns the value it is currently holding for that component instance. The destructured binding is created fresh on every call and is fixed for the duration of that call — a snapshot. Every closure created during that render (event handlers, effect callbacks, timers) captures that snapshot, which is why handlers see the value from the render that created them rather than the newest one. ## Why the assignment does nothing In an event handler — code that runs after the render has already produced DOM — assigning to the binding cannot change anything on screen, because the JSX for that render was built and committed before the handler ever ran. Two sub-cases: - With the usual `const [count, setCount] = ...`, `count = count + 1` throws a `TypeError`: assignment to a constant. Module and JSX-compiled code is strict-mode code, so this is a hard error, not a silent no-op. - With `let [count, setCount] = ...`, the assignment succeeds and updates a variable that is about to be garbage-collected along with the rest of that render's scope. React never observed it, so no render is scheduled, and on the next render `useState` returns the value React is still holding — your write is gone. The same applies to mutating what state holds: pushing into an array or setting a field on an object changes data React can see, but it still never told React to render. ## The setter is the only channel Calling `setCount(next)` does three things: it records the requested update against this component instance, it marks the component as needing to re-render, and it returns `undefined`. It does not assign to your `count` binding, and it does not run the component synchronously. That is why this logs the old number: ```jsx function handleClick() { setCount(count + 1); console.log(count); // still the value from this render } ``` The binding for the current render is immutable by design; the new value appears in the *next* render's binding. ## Two more properties worth knowing **The setter's identity is stable.** React guarantees the same setter function across renders of the same component, so you can safely omit it from a dependency array or pass it down to a memoized child without causing extra work. **State is per component instance.** Rendering `<Counter />` twice gives two independent counts. This is exactly why state cannot live in a module-level variable: that would be shared by every instance and invisible to React's scheduling. ## What to say in an interview Name the return shape (value plus setter), then say the sentence that shows the model: "the state variable is a snapshot of this render, not a live reference — the setter is what tells React to produce the next snapshot." If you can add that reading the variable immediately after calling the setter still gives the old value, and that the setter identity is stable, you have covered everything the question is probing.

  • After calling the setter, can you read the new value from the state variable on the very next line?
    No. That binding belongs to the render that is currently executing and never changes during it, so it still holds the old value. If you need the new value in the same handler, compute it into a local variable and use that, and pass the same expression to the setter. The updated value only shows up in the next render.
  • Does the setter function returned by useState change identity between renders?
    No — React guarantees it is stable for the lifetime of the component instance. That means you can leave it out of dependency arrays without the lint rule complaining, and pass it to a memoized child without breaking the memo. The state value, by contrast, is a new binding on every render.
  • Where does the state actually live, if not in the variable?
    React keeps it per component instance, alongside that instance in its internal tree, which is why two renders of the same component have separate counters and why unmounting discards the value. Your variable is only a per-render copy handed out by useState.

saying these in an interview costs you the question

  • Says useState returns an object with value and setter properties
  • Thinks assigning to the state variable re-renders after a short delay
  • Claims the setter mutates the destructured variable in place
  • Believes state is stored in the variable, so all instances share it
  • Says you can read the updated state on the next line after setting

context

open as a page

In a React component with `const [count, setCount] = useState(0)`, a click handler calls `setCount(count + 1)` three times in a row, yet each click only raises the count by one. Explain why, and what to write instead.

level: middleimportance: must knowfreq 78%

basics

~20 s

All three calls read the same count value from the current render, so all three request the same next value and the last one wins. Passing an updater function, setCount(c => c + 1), makes each call build on the previous result, giving plus three.

open as a page

A React component keeps a list in state. A handler runs `items.push(newItem); setItems(items);` and the rendered list never changes. What comparison is React making, why does `setItems([...items, newItem])` work instead, and can you rely on that comparison as a performance optimisation?

level: seniorimportance: must knowfreq 66%

basics

~20 s

React compares the next state with the current one using Object.is. Pushing mutates the same array, so the reference is identical and React bails out. A new array from the spread is a different reference, so the update is seen. Treat the bail-out as a courtesy, not a guarantee.

open as a page

In React, what is the difference between writing `useState(buildInitialState())` and `useState(buildInitialState)`, and when does that difference matter?

level: middleimportance: should knowfreq 52%

basics

~20 s

The first form calls buildInitialState on every render and throws the result away after the first. The second passes the function itself, so React calls it only during the initial render. That lazy form matters when the computation is expensive.

open as a page

A React component holds a callback in state with `const [handler, setHandler] = useState(() => defaultHandler)`. A later call to `setHandler(nextHandler)`, where `nextHandler` is also a function, leaves `handler` holding something unexpected. Why, and what should be written instead?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

A function argument means updater, not value. React calls nextHandler with the current state and stores whatever it returns, so the state becomes that return value. Store a function by wrapping it: setHandler(() => nextHandler).

open as a page