Course outline · 0% complete

0/28 lessons0%

Course overview →

Why components need state

lesson 4-1 · ~13 min · 10/28

Where changing data lives

Lesson 3-3 established that props are read-only, so a component never changes its own props. Data that changes over time, such as a click counter, has to live somewhere else, in a separate mechanism the component owns.

Props flow down from the parent and cannot be modified by the receiver, so they are the wrong home for anything a component needs to update. React calls the right tool state, and this unit is about it.

The distinction is worth fixing early: props are what a component was given, and state is what a component remembers. A task's title arrives as a prop, and whether the task list is currently filtered is state.

Why components need state

Try to build a counter with a plain variable:

function Counter() {
  let count = 0;
  return (
    <button onClick={() => { count = count + 1; }}>
      Clicked {count} times
    </button>
  );
}

Clicking does nothing visible. Two separate reasons:

  1. React does not know count changed, so it never re-renders. Nothing repaints the screen.
  2. Even if it did re-render, Counter would run again from the top and let count = 0 would reset it.

We need a variable that survives re-renders and tells React when it changes. That is exactly what useState provides.

useState

import { useState } from "react";

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

Read it piece by piece:

  • useState(0) declares a piece of state with initial value 0. React stores it outside your function, so it survives re-renders.
  • It returns a pair, unpacked with array destructuring from Advanced JavaScript: the current value (count) and a setter function (setCount).
  • Calling setCount(1) does two things: stores the new value, and schedules a re-render of this component.

useState is a hook: a function whose name starts with use and that gives your component access to a React feature, here, stored state. React ships a small set of them, and useEffect in Unit 7 is the other one this course needs.

Why hooks must stay at the top level

There is one hard rule about hooks: call them at the top level of the component, never inside an if, a loop, or a nested function. The rule exists because of how React stores your state. Your component is a plain function, so useState cannot attach state to a variable name. Instead, React keeps a list of state slots per component and matches each useState call to a slot by the order of the calls: first call gets slot 1, second call gets slot 2, and so on, every render.

const [name, setName] = useState("");     // always slot 1
const [count, setCount] = useState(0);     // always slot 2

Put a useState inside an if and some renders make two calls while others make one. The order shifts, slot 2's value lands in the wrong variable, and your name state suddenly holds a number. React detects this and throws an error rather than corrupting state, which is why the rule is enforced, not just recommended.

staterender()DOMsetCount(...)the render loopstate → UI → event → state
The render loop. State feeds render(), render() produces the DOM, a user event calls setCount, and the updated state triggers the next render. The gold dot traces the cycle.

What a setter call actually does

The moment setCount(5) is called, React stores 5 as the new state and schedules a re-render, and the next render sees count as 5.

The setter never edits the current render's variable. It records the new value and asks React to render again, then the component function re-runs, useState hands back 5, and the fresh JSX repaints the screen.

The word schedules is doing real work in that sentence. The re-render does not happen on the next line of your handler, so reading count immediately after calling setCount gives you the old value, which is the single most common surprise for people new to React.

That also explains why several setter calls in one handler produce one re-render rather than several. React batches the scheduled work and renders once with the final state, which is both faster and the reason the next lesson's triple-click puzzle behaves the way it does.

A hook inside a condition

function Form({ showEmail }) {
  const [name, setName] = useState("");
  if (showEmail) {
    const [email, setEmail] = useState("");
  }
  ...
}

The bug is that the second useState sits inside an if, so the number and order of hook calls changes between renders and React's slot matching breaks.

React matches each useState call to its stored slot by call order. When showEmail flips, the component makes a different number of hook calls, the order no longer lines up with the slots, and React throws an error rather than quietly handing you the wrong value.

There is a second, quieter bug in the same three lines. email and setEmail are declared with const inside the if block, so they do not exist outside it, and the state would be unreachable from the JSX below even if the hook rule allowed this.

The fix is to move both calls unconditionally to the top of the function, which is safe because holding an unused piece of state costs nothing:

const [name, setName] = useState("");
const [email, setEmail] = useState("");

The same rule rules out three related shapes: a useState inside a loop, one inside a nested function or a callback, and one placed after an early return. All four are the same mistake, which is making the sequence of hook calls depend on anything that can vary between renders.