Skip to content

Lesson 2 of 9 · 22m

Hooks Discipline: useState, useEffect, and the Rules That Matter

The rules of hooks aren't arbitrary — they exist because hooks are just an array of slots. Plus the useEffect habits that prevent 90% of effect bugs.

Hooks look like magic until you see the mechanism: each component instance owns an array of slots, and each hook call consumes the next slot in order. That's the whole reason for the two rules — always call hooks at the top level, and never inside conditions or loops. If a hook sat inside if, the slot order would change between renders and every state would slide one position down.

useState: keep it minimal

The single most common React bug isn't a hook misuse — it's redundant state:

// bad: stores something derivable
const [items] = useState(cart);
const [total, setTotal] = useState(0);
useEffect(() => setTotal(items.reduce((s, i) => s + i.price, 0)), [items]);

// good: compute it in render
const total = items.reduce((s, i) => s + i.price, 0);

Every piece of derived state is a synchronization bug waiting for an edge case. Ask of each useState: can this be computed? can it live higher? can it live lower? The correct answer is usually the lowest possible owner.

useEffect is for synchronization, not "on change"

useEffect is poorly named — it isn't "run when X changes," it's "keep an external system synchronized with props/state": subscriptions, timers, DOM APIs, third-party widgets. Fetching data, reacting to user edits, computing values — none of those need an effect.

// subscribing to something external — a real effect
useEffect(() => {
  const sub = channel.subscribe(onMessage);
  return () => sub.unsubscribe();   // cleanup runs before re-run and on unmount
}, [channel]);

The dependency array is a contract

Every reactive value the effect reads must be listed. Omitting one doesn't mean "don't re-run" — it means "re-run only if the other deps changed, then read a stale value." React 18+ with the exhaustive-deps lint rule will tell you when you lie; when it does, the fix is restructuring (often useCallback, or moving work into the event handler that triggered it), not silencing the rule.

::exercise(effect-deps)

Field notes

  • StrictMode double-invokes effects in dev to surface missing cleanups — a cleanup that breaks on a second mount was always a bug.
  • useRef is for values that persist across renders but shouldn't trigger them: DOM nodes, timers, previous values.
  • Custom hooks are just functions that call hooks — they share state between callers, never between instances.