Concept
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.
function Profile() {
const [name, setName] = useState("Ada"); // slot 0
const [age, setAge] = useState(36); // slot 1
useEffect(() => { document.title = name; }, [name]); // slot 2
// ...
}This single fact, slots are matched by call order, not identity, is the entire reason the Rules of Hooks exist.
The Rules of Hooks
- 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.
function Profile({ isEditing }) {const [name, setName] = useState("Ada"); // slot 0const [draft, setDraft] = useState(name); // slot 1useEffect(() => { ... }, [name]); // slot 2return /* ... */;}
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.
A quick tour of the built-ins
| Hook | What it gives you |
|---|---|
useState | A value that persists across renders, plus a setter that schedules a re-render (see State) |
useEffect | A way to synchronize with something outside React after commit (see Effects) |
useContext | Reads the nearest matching Provider's value without prop-drilling |
useRef |
Each of these is covered in depth in its own topic, this page is about the one mechanical rule that governs all of them.
Try It
Predict what happens before checking the solution.
function Settings({ showAdvanced }) {
const [name, setName] = useState("Ada");
if (showAdvanced) {
const [advancedMode, setAdvancedMode] = useState(false); // ⚠️
}
useEffect(() => {
console.log("settings changed");
}, [name]);
return /* ... */;
}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.
Implement It Yourself
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] = initialValue; // first render: initialize the slot
}
function setState(newValue) {
hookSlots[currentIndex] = newValue;
}
slotIndex++;
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.
Under the Hood
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).
Common Mistakes
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.
2. Calling a hook from a plain helper function
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).
3. Calling a hook after an early return
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.
Best Practices
- 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/useReducerholding an array or object, rather than calling a hook a variable number of times.
Performance Tips
- 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.
