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?
answer
- it hands back a pair
- the value is just a local binding
- assignment tells React nothing
- the setter is the only channel
- fresh binding on every render
basics
~20 suseState 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
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.
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.
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.
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