Rules of Hooks
Why hooks must be top-level and only in React functions — call order, the linter, and the bugs that break when you cheat.
- react
- hooks
- rules-of-hooks
Hooks look like normal functions. They are not. React associates each hook call with a slot in a fixed call order for that component instance. Break the order and React wires state, effects, and context to the wrong slots — silent wrong UI, not a helpful error every time.
The two rules are short. The why is what interviews and production bugs care about.
- Only call Hooks at the top level — not inside loops, conditions, or nested functions.
- Only call Hooks from React functions — function components or custom hooks (names starting with
use).
Official source: Rules of Hooks.
The model: ordered slots, not names
On every render, React walks the component body and records hook calls in the order they appear. Internally it’s closer to “hook #0, hook #1, hook #2” than “the one named count.”
function Profile() {
const [name, setName] = useState(''); // slot 0
const [age, setAge] = useState(0); // slot 1
useEffect(() => { /* ... */ }, [name]); // slot 2
// ...
}
Next render, React expects the same number of hooks in the same order. Slot 0 still feeds name. Slot 1 still feeds age. The labels in your source code are for you; the runtime only trusts position.
Custom hooks just append more slots in the order they call hooks:
function useFormField(initial) {
const [value, setValue] = useState(initial); // next slot for the caller
const [error, setError] = useState(null);
return { value, setValue, error, setError };
}
function Signup() {
const email = useFormField(''); // uses slots 0–1
const password = useFormField(''); // uses slots 2–3
// ...
}
That composition is why hooks scale — and why conditional hook calls destroy the mapping.
What breaks if you call hooks conditionally
// ❌ Never do this
function Search({ enabled }) {
if (!enabled) {
return <p>Search off</p>;
}
const [query, setQuery] = useState(''); // sometimes slot 0, sometimes missing
useEffect(() => {
// fetch...
}, [query]);
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
First render with enabled === true: two hooks. Next render with enabled === false: zero hooks. React’s previous state for slot 0 no longer matches this tree. You get invariant errors in development, and in bad cases state from the wrong hook after a structural change.
Correct pattern: always call the hook; branch the behavior.
// ✅ Hooks always run; UI / effects branch
function Search({ enabled }) {
const [query, setQuery] = useState('');
useEffect(() => {
if (!enabled || !query) return;
// fetch...
}, [enabled, query]);
if (!enabled) return <p>Search off</p>;
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
Same idea for early returns: put hooks above any return that depends on props/state.
Loops and nested functions
// ❌ Hooks inside map — count depends on list length
function List({ items }) {
return items.map((item) => {
const [selected, setSelected] = useState(false); // illegal
return <Row key={item.id} selected={selected} />;
});
}
Extract a child component. Each child instance gets its own hook list:
function Row({ item }) {
const [selected, setSelected] = useState(false);
return (
<button type="button" aria-pressed={selected} onClick={() => setSelected((s) => !s)}>
{item.label}
</button>
);
}
function List({ items }) {
return items.map((item) => <Row key={item.id} item={item} />);
}
Nested event handlers and useMemo factories also must not call hooks — they can close over values from hooks declared at the top of the component.
Only in React functions
Calling useState from a plain utility, a class method, or a random event module has no component fiber to attach to:
// ❌
export function formatUser(user) {
const [cache] = useState({}); // not a component or custom hook
// ...
}
Allowed call sites:
| Call site | OK? |
|---|---|
| Function component body | Yes |
Custom hook (use*) body |
Yes |
| Class component | No — use class state/lifecycle |
| Regular JS module / event handler / promise callback | No |
Custom hooks are how you share stateful logic without HOCs:
function useOnlineStatus() {
const [online, setOnline] = useState(
typeof navigator !== 'undefined' ? navigator.onLine : true
);
useEffect(() => {
const on = () => setOnline(true);
const off = () => setOnline(false);
window.addEventListener('online', on);
window.addEventListener('offline', off);
return () => {
window.removeEventListener('online', on);
window.removeEventListener('offline', off);
};
}, []);
return online;
}
The eslint plugin is not optional theater
eslint-plugin-react-hooks (rules-of-hooks + exhaustive-deps) catches almost every structural violation at edit time. Treat rule-of-hooks failures as blockers, not style nits.
If the linter flags a hook and your first instinct is // eslint-disable, stop. Restructure: child component, always-run hook + conditional body, or move non-React logic out of the hook call path.
Footguns that still sneak through
- Conditional custom hooks —
if (x) useThing()is the same bug as conditionaluseState. - Dynamic hook lists — “call N hooks for N filters” belongs in N components or one structure that always has a fixed max.
- Copy-pasting hooks below an early return after a refactor — easy regression; keep a consistent “hooks block then UI” layout.
- Thinking the rule is about purity only — purity matters for concurrent rendering, but the order rule is specifically about identity of state across renders.
Interview angle
Question: “Why can’t you call hooks inside if?”
Strong answer: React stores hook state in a list tied to call order for that fiber. Conditional calls change the length/order of the list between renders, so later hooks read the wrong memory. Fix by always calling hooks and branching inside them or in JSX after the hooks block.
Follow-ups you should own:
- How custom hooks compose (slots concatenate)
- Why index keys + remounting differ from hook rules (related identity topics — see Reconciliation)
- Difference between rules-of-hooks and exhaustive-deps
Related on this site
- Reconciliation & the Virtual DOM — fiber identity and keys
- useEffect Fundamentals — effect slots and cleanup
- Props vs State — what hooks usually manage
- Keys in Lists — list identity (often remounts hooks)
- JavaScript Interview Guide — broader prep map
Further reading
Related guides
- Custom Hooks DesignExtract reusable stateful logic with use* hooks — API design, composition, testing seams, and footguns that recreate render bugs.
- useEffect FundamentalsWhen useEffect runs, how cleanup works, what belongs in the dependency array, and the bugs from treating it like componentDidMount.
- useReducer for Complex StateWhen useReducer beats useState — action tables, pure reducers, dispatch stability, context pairing, and migration patterns.
- useRef for Values and DOMMutable boxes that survive renders — DOM refs, instance variables, avoiding re-renders, and the ref-vs-state decision.
- Accessibility Patterns in ReactPractical React a11y: labels, focus management, keyboard, live regions, and composition patterns that stay accessible.