Skip to the chapter
←ReactGGroundwork⚛React
React/Beginner/B5
5 / 57 written chapters
/

The cover
  1. B1Setup & JSX
  2. B2Thinking in React
  3. B3Components
  4. B4Props
  5. B5useState
  6. B6Events & conditionals
  7. B7Lists & keys
  8. B8Forms
  9. B9useEffect
  10. B10Lifting state & styling
  11. B11Guided project
  12. ★Cheat page
  1. I1useRef
  2. I2Context
  3. I3useReducer
  4. I4Custom hooks
  5. I5Effects in depth
  6. I6Routing
  7. I7State architecture
  8. I8Memoisation
  9. I9Boundaries & portals
  10. I10Data fetching
  11. I11Forms at scale
  12. I12State libraries
  13. I13Auth
  14. I14TanStack Query
  15. I15Testing
  16. I16TypeScript
  17. I17Animation
  18. I18E2E testing
  19. I19i18n
  20. I20File uploads
  21. I21Real-time UI
  22. I22shadcn/ui
  23. ★Cheat page
  1. A1Fiber
  2. A2Concurrent
  3. A3Suspense
  4. A4Server Components
  5. A5Server Actions
  6. A6use()
  7. A7Rendering strategies
  8. A8Effect timing
  9. A9External stores
  10. A10Refs in depth
  11. A11Component patterns
  12. A12Performance
  13. A13Accessibility
  14. A14Design systems
  15. A15Advanced TypeScript
  16. A16Architecture at scale
  17. A17Custom renderers
  18. A18Migrating & upgrading
  19. A19Production debugging
  20. A20Review & judgement
  21. A21State machines
  22. A22Feature flags
  23. A23Interview bank
  24. ★Cheat page

⚛B5 · Part 1 · Beginner

State with useState

State is memory between renders — and setting it is a request, not an assignment.

5 min read10 sections5 exercises
5 of 57

In this chapter

  • Why a plain variable does not work
  • A render is a snapshot
  • Batching
  • How React knows which state is which
  • Several states or one object?

and 5 more

about 5 min
10On this page
  1. Why a plain variable does not work
  2. A render is a snapshot
  3. Batching
  4. How React knows which state is which
  5. Several states or one object?
  6. State must be replaced, never mutated
  7. Nested state
  8. Bailing out of a render
  9. Lazy initial state
  10. What should be state, and what should not

Why a plain variable does not work

function Counter() {
  let count = 0;                          // reset on every render
  return <button onClick={() => count++}>{count}</button>;
}

Two separate problems. The variable is recreated each time the function runs, so it never remembers anything; and changing it does not tell React that anything happened, so nothing re-renders. State solves both.

const [count, setCount] = useState(0);

useState returns the current value and a function to request a new one. React keeps the value outside your function, so it survives across renders, and calling the setter is what schedules the next render.

A render is a snapshot

This is the idea that makes the rest of React click. Each render has its own count, fixed for the whole of that render. The value never changes mid-render.

function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
    console.log(count);   // still the old value — always
  }
}

setCount does not assign to count. It tells React "next time, use this", and React re-runs the component with a fresh count. Inside this call, count is a const and it is not going to move.

The classic three-clicks bug
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);   // count goes up by 1, not 3

All three read the same snapshot. If count is 0, all three say "make it 1". The fix is the updater form, which receives the latest value rather than closing over one:

setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);   // 3

Rule of thumb: if the next value is computed from the current one, use the updater form. It is never wrong, and it is required inside timers, intervals and any async callback.

Batching

React collects the state changes made during one event and re-renders once at the end. Three setState calls in a click handler produce one render, not three. Since React 18 that also applies inside promises, timeouts and native event handlers.

How React knows which state is which

useState does not take a name. So how does React tell two useState calls in the same component apart? By call order. It keeps a list per component instance and walks it in the same order every render.

const [name, setName] = useState("");   // slot 0
const [age, setAge] = useState(0);      // slot 1

Which is the entire reason for the rules of hooks. Put a hook inside an if and the call order changes between renders, so slot 1 returns the value that belonged to slot 0 — your age lands in your name.

if (loggedIn) {
  const [x, setX] = useState(0);   // ✗ shifts every slot after it
}
The rules of hooks, and why they exist
  • Only at the top level. Not in conditions, loops, or nested functions — call order must be identical on every render.
  • Only from React functions. Components or other hooks; there is no component instance to attach to otherwise.

Both are lint rules, and both catch real bugs rather than style. If you need conditional state, put the condition inside the component that owns it, or split it into two components.

Several states or one object?

// separate — usually better
const [name, setName] = useState("");
const [age, setAge] = useState(0);

// one object — when the fields always change together
const [form, setForm] = useState({ name: "", age: 0 });

Separate state is simpler to update and lets React skip renders more often. Group fields only when they genuinely move as a unit — a form's values, a coordinate pair — and remember that grouping means every update needs a spread.

State must be replaced, never mutated

React decides whether to re-render by comparing the old value with the new one by reference. Mutating an object gives it the same reference back, so it concludes nothing changed and skips the render.

// ✗ mutation — no re-render
user.name = "Ana";      setUser(user);
items.push(newItem);    setItems(items);
items.sort();           setItems(items);

// ✓ new value every time
setUser({ ...user, name: "Ana" });
setItems([...items, newItem]);
setItems([...items].sort());
OperationImmutable version
add to end[...items, item]
add to front[item, ...items]
remove by iditems.filter((i) => i.id !== id)
update oneitems.map((i) => (i.id === id ? { ...i, done: true } : i))
insert at index[...items.slice(0, i), item, ...items.slice(i)]
sort / reversecopy first: [...items].sort()

map, filter, slice and concat return new arrays. push, pop, splice, sort and reverse change the one you have. Knowing which is which is most of this.

Nested state

The spread is shallow, so a nested update needs a spread at every level it passes through:

setUser({
  ...user,
  address: { ...user.address, city: "Pune" },
});

If you are writing three levels of that, the state is shaped wrong. Flatten it, or split it into separate useState calls, or move to useReducer.

Bailing out of a render

If you set state to a value React considers equal — compared with Object.is — it may skip the re-render entirely.

setCount(5);      // count is already 5 → no re-render
setUser({ ...user });  // a new object → always re-renders

This is why spreading an object "just to be safe" is not free: an identical copy is a different reference, so React re-renders even though nothing changed. React may still render once more before bailing out, so do not treat it as a guarantee — treat it as a reason not to churn objects needlessly.

Lazy initial state

The argument to useState is evaluated on every render, even though it is only used on the first one. If it is expensive, pass a function instead — React calls it once.

useState(JSON.parse(localStorage.getItem("draft")));    // runs every render
useState(() => JSON.parse(localStorage.getItem("draft"))); // runs once

What should be state, and what should not

The most common cause of tangled React is state that should not exist. Before adding a useState, check:

QuestionIf yes
Can it be computed from props or other state?Compute it during render. Do not store it.
Does the UI change when it changes?If no, it should be a ref.
Does another component need it too?Lift it to the nearest common parent.
Is it the same value in two places?Delete one. Two sources of truth always drift.
const [items, setItems] = useState([]);
const [count, setCount] = useState(0);   // ✗ derived — will drift
const count = items.length;              // ✓ always right, free
Interview answer, one sentence

"State is a value React keeps between renders, and each render sees a fixed snapshot of it. Setting state schedules a re-render rather than assigning, which is why you use the updater form when the next value depends on the current one, and why state has to be replaced rather than mutated for React to notice at all."

05Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 9 testsApply a batch of updatersbeginner
  • 8 testsFlip one field, immutablybeginner
  • 8 testsA counter with a stepbeginner
  • 6 testsThe initialiser runs once, not every renderbeginner
  • 5 testsTwo updates in one click, correctlybeginner
All 46 for this level →
B5

Finished reading?

0 of 57 chapters read in this series

→Next stop

  1. B4Previous · B4Props
  2. B5you are here
  3. Next · B6Events & conditional renderingPass the function, do not call it — and know why && puts a zero on your page.4 min read

01On this page

10 sections · 0%

  1. 01Why a plain variable does not work
  2. 02A render is a snapshot
  3. 03Batching
  4. 04How React knows which state is which
  5. 05Several states or one object?
  6. 06State must be replaced, never mutated
  7. 07Nested state
  8. 08Bailing out of a render
  9. 09Lazy initial state
  10. 10What should be state, and what should not

[] chapters · t top