skip to content

In React, why must hooks be called in the same order on every render — what does React use to match a particular useState call to the state it returned last time?

level: middleimportance: must knowfreq 78%

answer

  1. hooks have no names
  2. the list is walked, not searched
  3. position is the identity
  4. a skipped call shifts everything after it
  5. same type means silent corruption

basics

~20 s

React stores each component's hooks as an ordered list and matches every call to a slot by position alone — hook calls carry no names or keys. Change the call order and a hook reads another hook's slot.

solid answer

~50 s

A function component body runs from scratch on every render, so nothing inside it survives. React keeps the surviving part outside: each mounted component owns an ordered list of hook records, one per hook call. On the first render every call appends a record; on every later render React resets a cursor to the head of that list and each hook call advances it one step, so the third `useState` in the body always gets the third record. There is no name, key, or variable identity involved — position *is* the identity. That is why a hook inside an `if`, a loop, or below an early `return` is illegal: on the render where the branch changes, every later hook shifts by one slot and starts reading state that belongs to a different call. React can sometimes catch it (`Rendered fewer hooks than expected.`), but when the shifted hooks happen to be the same type it just silently returns the wrong values.

go deeper

for a junior

Be able to state the rule plainly: hooks go at the top level of the component, never inside an if, a loop, or after an early return, and never in a plain callback.

for a middle

Explain the mechanism, not the rule: a per-component ordered list of hook records, a cursor reset each render, and the slot shift that a skipped call causes. Naming the two React errors it produces lands well.

for a senior

Show how the failure presents in production — silently wrong state when the shifted hooks share a type, and a crash only when the count drops. Say how you would spot it in review and why the lint rule is a hard gate, not advice.

for a principal

Own the trade: React bought automatic hook identity at the cost of one call-order rule, and everything downstream — the lint plugin, the compiler's Rules of React precondition — exists to defend it. Be ready to argue why keyed hooks would have been worse.

## The problem hooks have to solve A function component is an ordinary function that React calls again for every render. Every local variable in its body is recreated each time, so the function itself cannot remember anything. Yet `useState` must hand back on render 50 exactly the state it created on render 1. Two things therefore have to live outside the function: the stored values, and a rule for deciding which stored value belongs to which call. ## React's answer: a per-component ordered list Every mounted component has an internal object (its fiber) that owns a list of hook records — one record per hook call, in call order. Each record holds whatever that hook needs to persist: `useState` keeps the current value plus its pending-update queue, `useEffect` keeps the last dependency array and the cleanup function, `useMemo` keeps the cached value and its deps. On the first render the list is empty and each hook call appends a record. On every subsequent render React resets an internal cursor to the head of the list, and each hook call consumes the next record and moves the cursor forward. The Nth hook call in the body always meets the Nth record. ```jsx function Profile({ id }) { const [name, setName] = useState(''); // slot 0 const [open, setOpen] = useState(false); // slot 1 useEffect(() => { load(id); }, [id]); // slot 2 } ``` Nothing in that mapping refers to `name` or `open`. Rename the variables, and the slots are unchanged. The order of the calls is the entire addressing scheme. ## What a conditional call actually does ```jsx function Profile({ id }) { if (id) { const [name, setName] = useState(''); // slot 0 — but only sometimes } const [open, setOpen] = useState(false); // slot 0 or slot 1, depending on id } ``` While `id` is truthy the component mounts with two records. On the render where `id` becomes falsy the first call disappears, so `open` now consumes record 0 — the record created for `name`. It receives the name string as its state and its setter writes into the name slot. Nothing about that is detectable from the values alone: both are `useState` records, so React has no type mismatch to complain about. React does catch the cases it can see. If a render produces fewer hook calls than the previous one, it throws `Rendered fewer hooks than expected.` — the classic symptom of a hook below an early `return`. If the hook *types* at a slot differ between renders, development builds print a warning that begins `React has detected a change in the order of Hooks called by Profile`, followed by a table of previous versus current calls. Both are after-the-fact detections of a corruption that has already happened; neither is a safety net you should rely on. ## Why order rather than keys Keying hooks by name would mean either inferring an identity React cannot see (variable names vanish under minification, and a destructured tuple has no single name) or making every call site pass a unique string — bookkeeping at every line, and still ambiguous when a hook is called from a shared custom hook used twice in one component. Call order gives a stable, automatic identity for free, at the price of one rule the developer must keep. That is the trade React made, and it is the honest answer to "why can't I call a hook in an `if`?" ## Where the rule is enforced At runtime React points an internal dispatcher at the current renderer's hook implementations only while a component is rendering; calling a hook when no component is rendering finds no dispatcher and throws `Invalid hook call.` Statically, the `react-hooks/rules-of-hooks` rule from `eslint-plugin-react-hooks` approximates the rule by treating capitalized functions as components and `use`-prefixed functions as hooks, and flagging any hook call it finds inside a condition, loop, logical expression, `try`/`catch`/`finally`, nested callback, or after an early return. ## Two practical corollaries First, the *number* of hook calls must also be stable, not merely their relative order — a hook that only sometimes runs at the end of the body is just as broken as one in the middle. Second, a hook whose *arguments* are conditional is completely fine: the call must be unconditional, not its inputs. `useEffect(() => { if (!user) return; subscribe(user); }, [user])` obeys the rule perfectly, because the branch lives inside the callback, not around the call. One deliberate exception exists in React 19: `use` is specified to be callable inside conditions and loops, because it is not backed by a positional hook record in the same way.

  • Must the number of hook calls be identical every render, or only their relative order?
    Both. A hook that runs only on some renders changes the slot mapping for everything after it, and if the missing call is the last one React throws `Rendered fewer hooks than expected.` on the render where it vanishes — the usual sign of a hook placed below an early `return`. The stable-count requirement is why loops with variable length are also illegal call sites.
  • Is it also illegal to pass conditional arguments to a hook?
    No — the *call* must be unconditional, not its inputs. `useEffect(() => { if (!user) return; subscribe(user); }, [user])` is correct: the branch is inside the callback, the call itself runs every render. The same applies to a dependency computed from a ternary, or a state initializer chosen by a prop. Only the call site position is constrained.
  • Why doesn't React just let developers give each hook an explicit id?
    It would push per-call-site bookkeeping onto every line of every component, and the ids would still have to be unique across a component that calls the same shared hook twice. Minified variable names give React nothing to infer from either. Call order is an identity React can derive automatically with zero syntax, so the cost lands on one rule instead of on every call.

saying these in an interview costs you the question

  • Says React tracks hooks by variable or state name
  • Claims conditional hooks are safe if the condition rarely flips
  • Thinks React re-runs hooks keyed by the component's props
  • Believes only useState is order-sensitive, effects are not
  • Treats the rule as a lint style preference rather than a runtime constraint

context