Rules for using hooks in React
React Hooks rules define where and how you can call functions like useState and useEffect inside your app. Break them and React loses track of which state belongs to which component.
Theory
TL;DR
- Call Hooks only at the top level of a component or custom Hook, never inside conditions, loops, or nested functions
- Call Hooks only from React functional components or custom Hooks (functions starting with
use) - React tracks Hooks by call position, not by name. Same order every render = stable state
- Analogy: a restaurant that prepares dishes in a fixed sequence. Skip one step and the kitchen loses track of your whole order
- Decision rule: write all Hook calls first, then add conditions and loops below them
Quick example
// BAD: conditional hook call breaks the order
function BadComponent({ showCount }: { showCount: boolean }) {
if (showCount) {
const [count, setCount] = useState(0); // React loses slot tracking
}
return <div>{showCount ? count : 'Hidden'}</div>;
}
// GOOD: hook always runs, condition comes after
function GoodComponent({ showCount }: { showCount: boolean }) {
const [count, setCount] = useState(0); // Slot 1 - always called
return <div>{showCount ? count : 'Hidden'}</div>;
}BadComponent throws "Rendered more hooks than during the previous render". GoodComponent works on every render.
Why call order matters
React stores Hook state in a linked list attached to each component's fiber node. On the first render it builds that list: slot 1, slot 2, slot 3. On every re-render it walks the same list in the same order to restore each value. If a conditional skips slot 1, slot 2 gets data meant for slot 1 and everything shifts. The rule isn't arbitrary. It's a direct result of how React's fiber architecture works internally.
When to use
- Need state or a side effect in a component - add a Hook at the top, before any
returnstatement - Same stateful logic in multiple components - extract into a custom Hook named
useMyLogic, not a plain function - Conditional logic needed - put the condition inside the Hook call or after all Hook calls
- Loops - call one Hook outside the loop, store multiple values in an array or object keyed by index
- Event handlers - define them inside the component, but never call Hooks inside the handler body itself
Early returns are fine. if (!userId) return null placed after your useState calls is valid React 18 code. Hooks just need to complete before any conditional exits.
How React detects violations
React's internal dispatcher switches between mount and update phases per render. During update, it checks that the number of Hook calls matches the previous render count. A mismatch triggers an immediate error from ReactCurrentDispatcher. The eslint-plugin-react-hooks catches most violations statically before runtime, which is why it ships as standard in any React project setup.
Common mistakes
Hooks inside a condition
if (isLoggedIn) {
const [user, setUser] = useState(null); // shifts all subsequent slots
}Fix: call the Hook unconditionally first, then check isLoggedIn below.
Hooks inside an event handler
function handleClick() {
const [local, setLocal] = useState(0); // plain JS function, not tracked by React
}Fix: move state to the component level. Use useCallback if the handler needs memoization.
Hooks inside a loop
items.map(item => {
const [active, setActive] = useState(false); // new instances every render
return <li>{active ? 'on' : 'off'}</li>;
});Fix: one useState at the top, store values in an object keyed by item id.
Custom Hook without use prefix
function myFetchLogic() {
const data = useSWR('/api/data'); // ESLint won't flag violations inside this
}Fix: rename to useMyFetchLogic. The use prefix tells both React and ESLint this is a Hook.
Hooks outside a component
const globalCount = useState(0); // module scope, not trackedFix: move inside a component or into a custom Hook.
Real-world usage
- React core:
useStateanduseEffectin every functional component since React 16.8 - Next.js:
useSWRfrom Vercel for data fetching in app router pages - Redux Toolkit:
useSelectoranduseDispatchin any connected component - TanStack Query:
useQueryanduseMutation, over 2 million weekly downloads - Zustand:
useStorefor lightweight global state in Vercel-built apps
In my experience reviewing code, the "hooks inside a .map()" mistake shows up most often in teams migrating from class components. The fix is always the same: one Hook, one array outside the loop.
Follow-up questions
Q: Why does React use call order instead of Hook names or IDs?
A: Names aren't unique. Two useState calls in the same component would collide. Call position gives a unique, stable slot per Hook with no extra metadata needed.
Q: Can you call a Hook after an early return?
A: No. A Hook after a return gets skipped on that render, breaking the slot count. All Hooks must run before any conditional exits.
Q: What is a custom Hook and when should you create one?
A: A function starting with use that calls other Hooks inside. Create one when the same stateful logic repeats across two or more components.
Q: How does eslint-plugin-react-hooks know where Hooks are called?
A: It parses the AST and flags any function starting with use called inside a condition, loop, or non-component function. It also enforces the use naming convention for custom Hooks.
Q (senior): What exactly happens in React's fiber when Hook order changes between renders?
A: Each fiber stores a memoizedState linked list. On update, React reads nodes in sequence. A missing node means the current node holds data from the previous node. All subsequent reads are offset by one slot. React detects the count mismatch via ReactCurrentDispatcher and throws before returning inconsistent state to the component.
Examples
Basic: Counter with two state slots
function Counter() {
const [count, setCount] = useState(0); // Slot 1
const [label, setLabel] = useState('clicks'); // Slot 2 - always called
return (
<div>
<p>{count} {label}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
// Output: stable count and label across all renders. Slots never shift.Both Hooks run on every render. React restores count to slot 1 and label to slot 2 every time without confusion.
Intermediate: Todo list with filtering
function TodoList({ todos }: { todos: { id: number; text: string; done: boolean }[] }) {
const [filter, setFilter] = useState<'all' | 'active' | 'done'>('all'); // Slot 1
const visibleTodos = useMemo(
() => filter === 'all' ? todos : todos.filter(t => filter === 'done' ? t.done : !t.done),
[todos, filter]
); // Slot 2
return (
<div>
<select onChange={e => setFilter(e.target.value as any)} value={filter}>
<option value="all">all</option>
<option value="active">active</option>
<option value="done">done</option>
</select>
<ul>{visibleTodos.map(t => <li key={t.id}>{t.text}</li>)}</ul>
</div>
);
}
// Output: filter state survives re-renders. Memo skips recalculation when inputs haven't changed.useState at slot 1, useMemo at slot 2, no conditions around either call. The filtering logic lives inside useMemo, not wrapped around the Hook itself.
Advanced: Early return after Hooks
function Profile({ userId }: { userId: string | null }) {
const [profile, setProfile] = useState(null); // Slot 1 - runs every render
const [posts, setPosts] = useState([]); // Slot 2 - runs every render
if (!userId) return <div>No user selected</div>; // early return is fine here
// fetch and render logic only when userId exists
return <div>Profile for {userId}</div>;
}
// Output: slot count never changes because both useState calls complete before the conditional exit.Both useState calls run before the if (!userId) check, so React always counts two Hook slots. Flip the order and you break the rule immediately.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.