useState

a value react remembers between renders. the catch is that it is a snapshot of the render that read it, not a variable you update in place.

the value is frozen for that render

useState gives you two things: the value as it was when this render started, and a function that schedules the next render.

the first half is where everyone trips. this button adds 1:

<button onClick={() => setValue(value + 1)}>{value}</button>

and so does this one:

<button onClick={() => { setValue(value + 1); setValue(value + 1); }}>{value}</button>

both calls read the same value, the one captured when the component rendered. with value at 0, both schedule "set it to 1". the second call never sees the first.

the same thing bites anywhere that outlives the render: a setInterval, a promise callback, a listener registered once. they all keep reading the snapshot they closed over.

the updater form reads the current value

pass a function instead of a value and react hands you the state as it is at the moment the update is applied:

<button onClick={() => { setValue(v => v + 1); setValue(v => v + 1); }}>{value}</button>

now it adds 2.

if the next value depends on the previous one, use the function form. it costs nothing and removes the whole class of bug.

coming from plain javascript

it is tempting to read useState as a variable with extra steps:

let value = 1;
btn.addEventListener('click', () => { btn.textContent = ++value; });

here value really is one box everyone reads and writes. in react it is not, every render gets its own. carrying this mental model over is what produces the bugs above.

when not to use it