CodeOath
← All posts
React75 min total · 17 parts

React Fundamentals: Components, Hooks, and the Virtual DOM

Contents — Part 4 of 17: State with useState
Part 4 of 17 · ~2 min

State with useState

Props come from outside; state is data a component owns and can change over time, triggering a re-render when it does.

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

useState(0) returns a pair: the current value, and a setter function. Calling the setter doesn't mutate count in place — it tells React "schedule a re-render, and next time this component renders, this piece of state should be this new value." count itself, within any single render, is a plain constant — it never changes mid-render.

Why calling the setter twice doesn't add twice

function increment() {
  setCount(count + 1); // uses count from THIS render
  setCount(count + 1); // still uses count from THIS render — not count + 1!
}
// clicking once only adds 1, not 2

Both calls in the same event handler close over the same count — the value it was when this render's increment function was created. The fix is the functional updater form, which receives the latest pending state rather than the value captured at render time:

function increment() {
  setCount((c) => c + 1);
  setCount((c) => c + 1); // now correctly adds 2 — each updater sees the previous updater's result
}

Reach for the functional form whenever a state update depends on the previous value of that same state — it's correct regardless of how React batches or schedules the update, while reading the captured variable directly isn't.

State updates are batched

React batches multiple setState calls made during the same event handler (and, since React 18, during most other contexts too) into a single re-render, rather than re-rendering after each one:

function handleClick() {
  setCount((c) => c + 1);
  setFlag((f) => !f);
  // only ONE re-render happens, with both updates applied, not two
}

This is a performance optimization, not just an implementation detail — it's why you can't reliably read a just-updated state value on the very next line after calling its setter; the update is scheduled, not synchronous.

Don't mirror props into state

A common anti-pattern: copying an incoming prop into useState "just to have local state."

// Usually wrong — this copy immediately goes stale if `initialName` changes later
function Profile({ initialName }) {
  const [name, setName] = useState(initialName);
  ...
}

If the component should always reflect the current prop, just use the prop directly — don't duplicate it into state. State is for values this component itself changes over time (form input as the user types, a toggle, a counter) — not for re-storing something already being handed to it.