State

Hold a value that changes, update it correctly, and keep out of state everything that does not belong there.

07of 20
≈ 13Min read
8Parts

A variable does not work

State is two things at once: a value that survives re-renders, and a way to ask for a re-render. A plain variable is neither.

why useState exists at all
function Counter() {
  let count = 0;                          // reset on every render
  return <button onClick={() => count++}>{count}</button>;
  // the number changes in memory, and the screen never moves:
  // nothing told React to render again
}

useState

  • The argument is the initial value, used on the first render only. After that it is ignored.
  • The setter does not change count in the current render — count is a constant for that render. It schedules a new render where count is the new value.
  • If the new value is Object.is-equal to the old one, React skips the re-render entirely.
the whole API
const [count, setCount] = React.useState(0);

setCount(5);          // set it
setCount(count + 1);  // set it from the current render's value
Reading it right after setting it

setCount(1); console.log(count); logs the old value. That is not a bug: count belongs to the render it was read in. If you need the new value in the same handler, compute it into a variable first.

Update from the previous value

React batches the updates in a handler and applies them in order. The plain form captures the value from the render it was written in; the updater function receives the value the queue has reached. Use the updater whenever the new value depends on the old one.

the difference that bites everyone once
// three calls, one increment: all three read the same stale count
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);

// three calls, three increments: each receives the value so far
setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);

State is a snapshot

Every render has its own n, its own handlers and its own closures. The timeout captured the n that existed when the click happened, and two seconds later that is still what it holds. This is usually what you want — the code that ran belongs to the state it saw.

a timer proves it
function Late() {
  const [n, setN] = React.useState(0);

  function send() {
    setN(n + 1);
    setTimeout(() => alert(n), 2000);   // alerts the OLD n, always
  }

  return <button onClick={send}>{n}</button>;
}

Objects and arrays: replace, never mutate

React compares by identity
const [door, setDoor] = React.useState({ name: "Painted", days: 5 });

door.days = 10; setDoor(door);        // NOTHING HAPPENS: same object
setDoor({ ...door, days: 10 });       // a new object → a re-render

const [list, setList] = React.useState([]);

list.push(item); setList(list);       // nothing happens
setList([...list, item]);             // add
setList(list.filter((d) => d.id !== id));                        // remove
setList(list.map((d) => (d.id === id ? { ...d, days: 9 } : d))); // change one
setList(list.toSorted((a, b) => a.days - b.days));               // sort a copy
Nested objects need nested copies

{ ...state, door: { ...state.door, days: 9 } }. If you find yourself three levels deep, the state is probably shaped wrong — or it is time for a reducer.

Lazy initial state

for an expensive first value
// runs on EVERY render, and the result is thrown away after the first
const [rows, setRows] = React.useState(parse(bigString));

// runs once
const [rows, setRows] = React.useState(() => parse(bigString));

What does not belong in state

The test is simple: can I calculate it from what I already have? If yes, calculate it. Two sources of the same truth will disagree, and the bug will be reported as "it shows the old name sometimes".

Instead ofDo this
fullName alongside first and lastCompute it during render
filteredItems alongside items and queryCompute it during render
isEmpty alongside the listitems.length === 0
A prop copied into stateUse the prop; state will go stale when it changes
A value nothing on screen depends onA ref, or an ordinary variable

Grouping

Shaping state so that impossible combinations cannot be written down removes a whole class of bug before it exists.

separate when they change separately
// fine: independent things
const [query, setQuery] = React.useState("");
const [sort, setSort] = React.useState("name");

// better as one: they always change together
const [status, setStatus] = React.useState({ state: "idle", error: null });

// impossible states: these two can both be true
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState(null);
// one field instead: "idle" | "loading" | "error" | "ready"

Practice