A hook is just a function whose return value depends on a hidden, ordered list attached to the component instance — read by CALL ORDER, not by name. Every 'rule' of hooks falls directly out of that one mechanical fact.
hooksrules-of-hooksuseStateuseEffectfundamentals
Knowledge Check
4 questions · pass at 70%
0/4
easymcq
1. How does React know which stored value a given useState() call should return on re-render?
Interview Questions
2 questions
Cheatsheet
Download-ready reference
Cheatsheet
Hooks are stored in an ORDERED LIST per component instance —
matched by CALL ORDER (slot position), never by name or identity.
Rules of Hooks:
1. Only call hooks at the TOP LEVEL — never in a condition, loop,
or nested function.
2. Only call hooks from a React function component or another
custom hook — never from a regular helper function.
WHY: skipping a hook call on some renders shifts every hook call
AFTER it to the wrong slot → corrupted state or a thrown error
("Rendered fewer hooks than expected" / order of Hooks changed).
Fix for "I need this conditionally": push the condition INSIDE the
hook's callback/body, never around the hook CALL itself.
✅ useEffect(() => { if (!flag) return; ... }, [flag])
❌ if (flag) { useEffect(() => { ... }, []) }
Custom hooks are just regular functions that call other hooks —
the `use` prefix is a CONVENTION for tooling, not a language rule.
The beginner framing: hooks are special functions — useState, useEffect, useContext, and others — that let a function component do things only class components used to be able to do: remember values across renders, run code after rendering, and tap into shared data from further up the tree.
The precise mental model: for each component instance, React keeps a hidden, ordered list of "hook slots" — one entry per hook call, in the exact order those hooks were called during the first render. On every subsequent render, React walks that same list in call order and hands each hook call the state that lives in its slot. There's no lookup by variable name or hook type involved — slot 0 is whatever was called first, slot 1 is whatever was called second, and so on, forever, for the lifetime of that component instance.
Only call hooks at the top level. Never inside a condition, loop, or nested function.
Only call hooks from React function components or from other custom hooks. Never from a regular helper function or an event handler.
These aren't stylistic guidelines — they're the only way to guarantee the same hooks run in the same order on every render, which is the sole thing that makes the slot list work at all.
Hooks are read by call order — a conditional hook shifts every slot after it
Hooks are stored in an ordered list — a "slot" array — per component instance, indexed purely by the order they're CALLED in, not by name or variable. This safe version calls all three hooks unconditionally, every render.
What happens if showAdvanced is true on the first render, then becomes false on the second?
Solution
React throws an error — something like "Rendered fewer hooks than expected" or "React has detected a change in the order of Hooks." On the first render, showAdvanced is true, so all three hooks run: slot 0 (name), slot 1 (advancedMode), slot 2 (useEffect). On the second render, showAdvanced is false — the if block is skipped, so only two hook calls happen: slot 0 (name), and then useEffect lands in slot 1, the slot React has on record as a useState call from last render. React can't reconcile a useState slot with an effect call, so it throws rather than silently corrupting state. Walk through the exact mismatch step-by-step in the visualizer above.
Build a minimal version of the hook slot mechanism — a tiny useState clone that shows exactly why call order matters:
let hookSlots = [];let slotIndex = 0;function resetHooksForRender() { slotIndex = 0; // rewind to the start of the list before each render}function useState(initialValue) { const currentIndex = slotIndex; // capture THIS call's slot, by position if (hookSlots[currentIndex] === undefined) { hookSlots[currentIndex]
Notice useState never receives any identifier for which piece of state it's returning — only slotIndex, which is purely a function of call order. This is precisely why skipping a hook call on some renders (but not others) silently shifts every subsequent slot.
The hook slot list is, at its core, an ordered list very similar to the arrays and linked lists covered in Data Structures — React's actual Fiber implementation stores hooks as a linked list attached to the fiber node, walked one node at a time on every render. And the reason a hook can "remember" a value across calls at all is the same mechanism as Closures: the slot list itself is kept alive across renders (much like a makeCounter()-style closure keeps its enclosing scope alive), even though each render is, mechanically, a brand-new call to the component function (see Execution Context).
1. Calling a hook inside a condition, loop, or nested function#
if (isLoggedIn) { const [user, setUser] = useState(null); // ❌ conditional}items.forEach(() => { useState(0); // ❌ inside a loop});
Covered above in depth — any hook call that doesn't happen unconditionally, in the same order, every render, risks a slot mismatch. Fix: call the hook unconditionally at the top level, and put the conditional logic inside the hook's body or around how its value is used, not around the call itself.
function getUserDisplayName() { const [name] = useState("Ada"); // ❌ not a component, not a custom hook return name;}
Hooks rely on React tracking "which component/hook is currently rendering" — a plain function called from anywhere (including outside render) has no such tracking. Only call hooks directly inside a function component's body or another custom hook (see Custom Hooks).
function Profile({ user }) { if (!user) return null; // returns HERE... const [tab, setTab] = useState("overview"); // ❌ ...so this hook is sometimes skipped return /* ... */;}
This is the conditional-hook mistake in disguise — the useState call only happens on renders where user is truthy, which is exactly the kind of call-order inconsistency the Rules of Hooks forbid.
Install eslint-plugin-react-hooks (bundled with most React toolchains) — it statically catches conditional/looped hook calls and missing effect dependencies before they ever reach runtime.
Name every custom hook starting with use — this isn't just convention, it's what lets the lint rule (and other engineers) recognize it's subject to the Rules of Hooks in the first place.
Push conditional logic inside the hook, not around the call — useState(isAdvanced ? {} : null) is fine; if (isAdvanced) { useState() } is not.
Keep the number and order of hook calls independent of props/state — if you need a variable number of stateful values, store them in a single useState/useReducer holding an array or object, rather than calling a hook a variable number of times.
The slot-list mechanism itself is cheap — the cost of hooks comes from what's inside them (an expensive computation in useMemo, an expensive effect), not from the bookkeeping of the list.
Because slot identity depends only on call order, adding or removing a hook call conditionally is a correctness bug long before it's a performance concern — always resolve a "changed order of Hooks" warning immediately rather than treating it as noise.
A mutable box that persists across renders and does NOT trigger a re-render on write (see Refs & the DOM)
useReducer
Like useState, but state transitions go through a reducer function — useful once update logic gets complex
useMemo / useCallback
Cache a computed value / function reference across renders, given the same dependencies
(
"settings changed"
);
}, [name]);
return /* ... */;
}
=
initialValue;
// first render: initialize the slot
}
function setState(newValue) {
hookSlots[currentIndex] = newValue;
}
slotIndex++; // advance to the next slot for the NEXT hook call this render
return [hookSlots[currentIndex], setState];
}
// Simulating two renders of a component that calls useState twice: