React · deeper

Where does useState keep the value?

Not in your function; that dies every render. React keeps a hidden list per component instance. Each hook call, in order, gets the next slot. First useState → slot 0. Second → slot 1. useEffect → slot 2.

On the next render, React walks the same list in the same order and hands each call its slot back. Press render a few times. The calls change; the slots don't.

the hidden list, one component instance
const [name, setName] = useState("Ada") → slot 0
const [open, setOpen] = useState(false) → slot 1
useEffect(() => log(name), [name]) → slot 2
"Ada"slot 0
falseslot 1
effectslot 2 · deps ["Ada"]

render #1

Therefore

That's why hooks can't go inside an if.

The slots are matched by position, nothing else. If a hook is skipped on one render, every hook after it shifts up one slot and reads the wrong value.

Put the first useState inside a condition and toggle it off. Watch open receive "Ada".

the rule is mechanical, not stylistic
if (cond) { const [name, setName] = useState("Ada") } → slot 0
const [open, setOpen] = useState(false) → slot 1
useEffect(() => log(name), [name]) → slot 2
"Ada"slot 0
falseslot 1
effectslot 2

All aligned. Now flip the condition.

Place your bet

With an empty dependency array, when does the effect run?

Once. (In next dev you'll see it twice: React's Strict Mode mounts, unmounts and remounts every component on purpose to catch effects that don't clean up. Production runs it once.) The array lists what the effect depends on. React compares it to last render's array; if nothing changed, it skips. An empty array never changes, so: first render only. No array means "no comparison", so: every render. This is the mechanical reason for the three idioms on the right.

bet, then play with the deps
deps decide when it fires
deps:
The last piece

A custom hook is a function that calls hooks. Nothing more.

Because the slots are matched by call order, a function that calls useState twice just takes two slots from the same list. Name it use… so the linter can check the rules inside it, and you have a custom hook.

This is how useUser(), useDebounce(), and every hook a library gives you work. They're not a special kind of thing. They're bundles of slots.

useToggle, in full
function useToggle(initial = false) {
  const [on, setOn] = useState(initial);      // takes a slot
  const toggle = () => setOn(v => !v);
  return [on, toggle];
}

function Menu() {
  const [open, toggle] = useToggle();       // slot 0, via the hook
  const [name] = useState("Ada");           // slot 1
  …
}

The rules of hooks are one rule: keep the call order stable.

Top of the function, never in a loop or condition, only from components or other hooks. All three are the same sentence said three ways.