
The Rules of Hooks Explained with Real-World Examples
"Rendered more hooks than during the previous render." If you've seen that error, you've broken one of the rules of hooks. It usually happens in code that looks perfectly reasonable: an early return for a loading state, a useEffect inside an if, a hook called inside a map over list items.
There are only two rules, and they fit in one sentence: call hooks at the top level, and only from React functions. What makes them stick is understanding why React needs them. Once you see how React keeps track of hooks, the rules stop feeling arbitrary and the fixes become obvious.
This post explains how React stores hook state, walks through each rule with the real-world violations people actually write, shows how to restructure each one, and covers the exceptions, including React 19's use, which breaks the pattern on purpose.
The Two Rules
- Only call hooks at the top level. Don't call them inside conditions, loops, nested functions,
try/catchblocks, or after an early return. - Only call hooks from React functions. That means function components and custom hooks. Not regular JavaScript functions, not class components, not event handlers.
"Hooks" here includes built-ins like useState, useEffect, and useContext, plus every custom hook whose name starts with use.
Why React Needs These Rules
React doesn't know your hooks by name. When you write:
import { useState } from "react";
export function Profile() {
const [name, setName] = useState("Ada");
const [age, setAge] = useState(36);
const [city, setCity] = useState("London");
// ...
}
React stores the three state values in a list attached to this component instance, in call order. On every render, it walks the list again: the first useState call gets slot 1, the second gets slot 2, the third gets slot 3. There are no labels, just positions.
Here's a heavily simplified model of what React does internally:
let hooks: unknown[] = [];
let index = 0;
function useStateSimplified<T>(initial: T) {
const i = index++;
if (hooks.length <= i) hooks[i] = initial;
const setState = (next: T) => {
hooks[i] = next;
// ...schedule a re-render
};
return [hooks[i] as T, setState] as const;
}
function render(component: () => void) {
index = 0; // reset before each render
component();
}
Now imagine a hook call is skipped on one render. Every hook after it shifts up by one position. The second useState reads the first slot's value, the third reads the second's, and React's internal checks throw an error when the number or type of hooks doesn't match. That's the entire reason for rule 1: the order of hook calls must be identical on every render.
Rule 2 exists because hooks need a component instance to attach that list to. A regular function called from an event handler has no instance, so there's nowhere to store the state.
Violation 1: Hook Inside a Condition
A common version: only subscribe when a feature is enabled.
// Broken
import { useEffect, useState } from "react";
export function Notifications({ enabled }: { enabled: boolean }) {
if (enabled) {
useEffect(() => {
const id = setInterval(checkForNew, 30_000);
return () => clearInterval(id);
}, []);
}
const [count] = useState(0);
return <span>{count}</span>;
}
declare function checkForNew(): void;
When enabled flips from false to true, the number of hooks changes and React throws. Fix: always call the hook, and put the condition inside it.
import { useEffect, useState } from "react";
export function Notifications({ enabled }: { enabled: boolean }) {
const [count] = useState(0);
useEffect(() => {
if (!enabled) return;
const id = setInterval(checkForNew, 30_000);
return () => clearInterval(id);
}, [enabled]);
return <span>{count}</span>;
}
declare function checkForNew(): void;
The effect always exists, it just does nothing when disabled. As a bonus, enabled is now a dependency, so turning the feature off cleans up the interval.
Violation 2: Early Return Before a Hook
This is the most common one in practice, because it looks so natural:
// Broken
import { useMemo } from "react";
type Order = { id: string; total: number; items: { name: string }[] };
export function OrderSummary({ order }: { order: Order | null }) {
if (!order) {
return <p>Loading...</p>;
}
const itemNames = useMemo(
() => order.items.map((i) => i.name).join(", "),
[order],
);
return <p>{itemNames}: ${order.total}</p>;
}
On the first render, order is null, the component returns early, and useMemo is never called. When the order arrives, useMemo is suddenly called: more hooks than the previous render.
Fix: call all hooks before any early return, handling the missing data inside:
import { useMemo } from "react";
type Order = { id: string; total: number; items: { name: string }[] };
export function OrderSummary({ order }: { order: Order | null }) {
const itemNames = useMemo(
() => order?.items.map((i) => i.name).join(", ") ?? "",
[order],
);
if (!order) {
return <p>Loading...</p>;
}
return <p>{itemNames}: ${order.total}</p>;
}
An alternative fix is to split the component: a parent that handles loading and a child that only renders when data exists. The child can then call its hooks unconditionally with non-null data:
export function OrderSummary({ order }: { order: Order | null }) {
if (!order) return <p>Loading...</p>;
return <OrderDetails order={order} />;
}
function OrderDetails({ order }: { order: Order }) {
const itemNames = useMemo(() => order.items.map((i) => i.name).join(", "), [order]);
return <p>{itemNames}: ${order.total}</p>;
}
Splitting is often cleaner, since the child's types no longer include null. For more on structuring loading UI, see handling loading and error states elegantly in React.
Violation 3: Hooks Inside a Loop
You have a list of items and want state for each one:
// Broken
import { useState } from "react";
export function Checklist({ items }: { items: string[] }) {
const checked = items.map(() => useState(false));
// ...
}
If items grows or shrinks, the number of hooks changes. Fix option 1: move per-item state into a child component. Each instance has its own hook list:
import { useState } from "react";
function ChecklistItem({ label }: { label: string }) {
const [checked, setChecked] = useState(false);
return (
<li>
<label>
<input
type="checkbox"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>
{label}
</label>
</li>
);
}
export function Checklist({ items }: { items: string[] }) {
return (
<ul>
{items.map((item) => (
<ChecklistItem key={item} label={item} />
))}
</ul>
);
}
Fix option 2: store all items' state in one hook. Useful when the parent needs to know which items are checked:
import { useState } from "react";
export function Checklist({ items }: { items: string[] }) {
const [checked, setChecked] = useState<Set<string>>(() => new Set());
function toggle(item: string) {
setChecked((prev) => {
const next = new Set(prev);
if (next.has(item)) next.delete(item);
else next.add(item);
return next;
});
}
return (
<div>
<p>
{checked.size} of {items.length} done
</p>
<ul>
{items.map((item) => (
<li key={item}>
<label>
<input
type="checkbox"
checked={checked.has(item)}
onChange={() => toggle(item)}
/>
{item}
</label>
</li>
))}
</ul>
</div>
);
}
Both are valid. Choose based on who needs to read the state. The key prop matters in either version, as covered in why keys matter when rendering lists in React.
Violation 4: Hooks in Event Handlers and Callbacks
// Broken
import { useContext } from "react";
import { ThemeContext } from "./theme";
export function ThemeLogger() {
function handleClick() {
const theme = useContext(ThemeContext); // not allowed
console.log(theme);
}
return <button onClick={handleClick}>Log theme</button>;
}
Event handlers run outside rendering, so there's no hook list to read from. Fix: call the hook at the top level and use the value in the handler.
import { useContext } from "react";
import { ThemeContext } from "./theme";
export function ThemeLogger() {
const theme = useContext(ThemeContext);
function handleClick() {
console.log(theme);
}
return <button onClick={handleClick}>Log theme</button>;
}
The same applies to callbacks passed to useMemo, useEffect, useReducer, and array methods. You can't call useState inside a useEffect callback.
Violation 5: Hooks in Regular Functions
// Broken
import { useState } from "react";
function getCartTotal(items: { price: number }[]) {
const [discount] = useState(0); // regular function, not a hook
return items.reduce((sum, i) => sum + i.price, 0) - discount;
}
Fix: either remove the hook (if the function should be a pure helper) or rename it to start with use, making it a custom hook that must itself follow the rules:
import { useState } from "react";
export function useCartTotal(items: { price: number }[]) {
const [discount, setDiscount] = useState(0);
const total = items.reduce((sum, i) => sum + i.price, 0) - discount;
return { total, setDiscount };
}
The rename isn't cosmetic. The use prefix is how the linter knows to check the function's body and its call sites. More on this in building your own custom hooks in React.
The Exception: use()
React 19 introduced use, which reads a promise or a context. Unlike other hooks, use can be called inside conditions and loops:
import { use } from "react";
import { ThemeContext } from "./theme";
export function Heading({ show, children }: { show: boolean; children: string }) {
if (!show) return null;
const theme = use(ThemeContext); // allowed after an early return
return <h2 className={theme}>{children}</h2>;
}
That's because use doesn't store anything in the component's hook list. It reads from context or a promise each time. It still must be called during rendering, inside a component or hook, and not inside try/catch. Exploring the React use() hook covers it in depth.
Let the Linter Enforce the Rules
You don't need to catch these by eye. eslint-plugin-react-hooks has a rules-of-hooks rule that reports every violation in this post:
npm install -D eslint-plugin-react-hooks
// eslint.config.js
import reactHooks from "eslint-plugin-react-hooks";
export default [reactHooks.configs.flat.recommended];
Most React templates include it already. Treat its errors as real bugs, not style warnings. The React Compiler also relies on these rules, and skips optimizing components that break them.
Best Practices for Following the Rules of Hooks
- Put all hook calls at the top of the component, before any conditions or returns. It's an easy visual check.
- Move conditions inside hooks, not around them.
- Extract child components for per-item state or for data that's only present after loading.
- Prefix only real hooks with
use, and keep plain helpers as plain functions. - Keep
rules-of-hooksset to error, never warn, and never disable it inline.
Frequently Asked Questions (FAQ) About the Rules of Hooks
React counted a different number of hook calls than on the previous render, almost always because a hook is inside a condition or after an early return. Move every hook call above any conditional logic so the same hooks run in the same order every time.
No. Hooks can't be called inside callbacks, including effect callbacks. Call the hook at the top level of the component and use its value inside the effect.
No. Hooks only work in function components and custom hooks. To use hook-based logic in a class, wrap the class in a small function component that calls the hook and passes the result as props.
use doesn't occupy a slot in the component's ordered hook list. It reads a context or a promise directly each time it's called, so skipping it on some renders doesn't shift any other hook's position.
No. A custom hook's body follows the same rules as a component. Conditions belong inside the hooks it calls, or as parameters like enabled that control what the hook does.
The order must be the same on every render, but you can arrange hooks in any order you like. It's just that once you choose an order, it can't change between renders of the same component.
Conclusion
React identifies hooks by call order, not by name. That's why hooks must be called at the top level of a function component or custom hook, unconditionally, in the same order every render. Most violations come from early returns, conditions, loops, and event handlers, and each has a standard fix: move conditions inside hooks, call hooks before returns, extract child components, or read the value at the top level and use it in the handler.
Make sure eslint-plugin-react-hooks is enabled in your project and treat its rules-of-hooks errors as bugs. With the linter catching mistakes and the call-order model in your head, the rules become second nature quickly.


