Topic 4.3
The Rules of Hooks
In one line
Call hooks only at the top level of a component or a custom hook, never inside conditions, loops, nested functions or after an early return, because React matches each hook to its saved state purely by the order of the calls.
Think of it like this
A row of school lockers with numbers but no names. Every morning the children line up in the same order, and the first child opens locker 1, the second opens locker 2, and so on. It works perfectly as long as the line never changes. If one day the second child stays home, the third child opens locker 2 and finds someone else's lunch. React stores hooks the same way: by position in the line, not by name.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Hook
- A function whose name starts with
usethat adds a React feature to a component. - Top level
- Directly in the component's body, not inside an
if, a loop or another function. - Call order
- The order in which your hooks run during a render: first, second, third…
- Slot
- The place where React keeps one hook's saved data, found by its position in the call order.
- Early return
- A
returnin the middle of a component that stops the rest of the function from running. - Custom hook
- Your own function whose name starts with
useand that calls other hooks (Phase 10). - ESLint
- A tool that reads your code and flags mistakes without running it.
- eslint-plugin-react-hooks
- The official ESLint plugin from the React team that checks the Rules of Hooks.
Step by step
01React saves hooks in a list
On the first render of Form, React creates one slot per hook call, in order. It doesn't store the variable names; it can't see them.
02Every render reads the slots in the same order
On the next render, the first useState gets slot 1, the second gets slot 2, and so on. As long as the calls happen in the same order, every variable gets its own value back.
03A skipped hook shifts everything after it
Put the second useState inside an if. When the condition is false, the third call (city) receives slot 2, which holds the nickname. The live mini engine below shows exactly that.
function Form({ showNickname }) {
const [name] = useState("Asha");
if (showNickname) {
const [nick] = useState("Ash"); // ❌ sometimes skipped
}
const [city] = useState("Pune"); // gets the wrong slot when skipped
// ...
}04Fix it: unconditional hook, conditional use
Call the hook every time and decide what to show afterwards.
function Form({ showNickname }) {
const [name] = useState("Asha");
const [nick] = useState("Ash"); // ✅ always called
const [city] = useState("Pune");
return <p>{name}{showNickname && " (" + nick + ")"} from {city}</p>;
}05Or move the conditional part into its own component
Rendering a component conditionally is allowed. Its hooks belong to it: when it's shown it calls all its hooks; when it's hidden it doesn't exist at all.
function Profile({ user }) {
const [likes, setLikes] = useState(0);
return (
<>
<button onClick={() => setLikes(likes + 1)}>Like {likes}</button>
{user && <Tabs />} {/* Tabs has its own useState inside */}
</>
);
}06Let ESLint catch it
With eslint-plugin-react-hooks enabled, every broken rule is underlined in your editor. These are the real messages the rule prints.
$ npx eslint src/Profile.jsx
/home/asha/app/src/Profile.jsx
7:25 error React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return? react-hooks/rules-of-hooks
13:29 error React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render react-hooks/rules-of-hooks
16:23 error React Hook "useState" is called in function "handleClick" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use" react-hooks/rules-of-hooks
✖ 3 problems (3 errors, 0 warnings)Try it yourself
- 1
Break the mini engine differently
In the imitation, render with
showNickname: falsefirst andtruesecond. Predict both lines before running, then addconsole.log("nick:", nick)inside theif. (Both results look right, butnickisPune: on the second render the nickname call takes slot 1, which already holds the city. The bug is hiding, not gone.) - 2
Return early before a hook
In the fixed example, move
const [likes, setLikes] = useState(0);below theif (!loggedIn) { return ... }. Predict what happens after the login click, then run. You'll see React's hook-order table again. - 3
Use the hook unconditionally
In the broken example, move
const [tab, setTab] = useState("posts");above theifand show{loggedIn && <p>Tab: {tab}</p>}in the JSX. The error disappears.
Code & diagrams
On the second render `city` reads slot 1, which holds the nickname. Real React is more careful: it notices the number of hooks changed and throws instead of silently mixing values.
Console
(nothing was printed)Expected console output
{ name: 'Asha', city: 'Pune' }
slots: [ 'Asha', 'Ash', 'Pune' ]
{ name: 'Asha', city: 'Ash' }The first render calls two hooks. After the click, the `if` is true and the render calls three. React prints a table of the hook order, then throws. The page goes blank because nothing caught the error (error boundaries come in Topic 14.5).
Preview
Console
Nothing logged yet.Expected console output
Profile rendered, loggedIn = false likes = 0
React has detected a change in the order of Hooks called by Profile. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://react.dev/link/rules-of-hooks
Previous render Next render
------------------------------------------------------
1. useState useState
2. useState useState
3. undefined useState
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Uncaught Error: Rendered more hooks than during the previous render.
An error occurred in the <Profile> component.
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://react.dev/link/error-boundaries to learn more about error boundaries.Profile calls the same two hooks on every render. `Tabs` appears only after logging in, and that's allowed: its hook belongs to `Tabs`, not to `Profile`.
Preview
Console
Nothing logged yet.Expected console output
Profile rendered, loggedIn = false likes = 0
Profile rendered, loggedIn = true likes = 0
Profile rendered, loggedIn = true likes = 1Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Call a hook inside an event handler
Inside handleClick, write const [x, setX] = useState(0); and click the button.
Break #2
Return early before a hook
Put if (!user) return <p>Please log in</p>; above a useState call, then log the user out after they were logged in.
Myth vs fact
Myth
The Rules of Hooks are just style advice.
Fact
They follow from how hooks are stored: by call order. Break them and state ends up in the wrong variable or React throws.
Myth
You can't show anything conditionally if you use hooks.
Fact
You can render conditionally as much as you like. Only the hook calls must be unconditional; the values they return can be used inside any if.
Myth
A function whose name starts with use is automatically a hook.
Fact
The use prefix is a naming convention that tells people and ESLint "this calls hooks". The function is a custom hook because it calls hooks, and the name lets the linter check it.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Internally each component's fiber keeps its hooks as a linked list. During an update, React walks the list in step with your calls; if the hook type at a position changes (say
useStatethenuseEffect), the development build prints the order table you saw. - ▸
use(React 19) is the one exception: it can be called insideifstatements and loops, because it doesn't keep a slot of its own state between renders. It must still be called from a component or hook, not from an event handler (Phase 14). - ▸
The React Compiler (1.0) relies on the Rules of React (pure renders, hooks at the top level). Code that breaks them is skipped by the compiler, which is one more reason the ESLint rules ship in the plugin's
recommendedpreset. - ▸
Invalid hook callin a project that follows the rules almost always means two copies ofreact(for example, a linked library with its ownnode_modules/react).npm ls reactshows them.
Remember this
- 1
A Hook is any function whose name starts with
use:useStatenow, lateruseEffect,useRef,useContext,useReducerand your own custom hooks (Phase 10). Hooks only work while React is rendering a component, because they read and write that component's saved data. - 2
Rule 1: call hooks at the top level. Call them directly in the body of your component, before any
return, every single render. Don't call them insideifstatements, loops,&&/?:expressions, nested functions, event handlers, after an earlyreturn, insidetry/catch/finally, or inside the callbacks you pass to other hooks. - 3
Rule 2: call hooks only from React functions: from function components (names starting with a capital letter) or from custom hooks (names starting with
use). Not from regular JavaScript functions, not from class components, not from event handlers. Calling one in the wrong place throwsInvalid hook call. - 4
Why: hooks are matched by call order. React gives your component no ids for its hooks. On the first render it creates one slot per hook call, in order: slot 1 for the first
useState, slot 2 for the second. On every later render it hands out the slots again in the same order. If a hook is skipped (anifwas false this time), every hook after it receives the wrong slot: one variable gets another's value. React notices when the number of hooks changes and throwsRendered more hooks than during the previous render.(orRendered fewer hooks than expected.). - 5
How to fix a conditional hook: keep the hook call unconditional and put the condition inside or after it. Instead of
if (loggedIn) { const [tab, setTab] = useState("posts"); }, writeconst [tab, setTab] = useState("posts");at the top, and usetabonly whenloggedInis true. Instead of returning early before a hook, move the earlyreturnbelow all the hooks. If a part of the screen needs its own hooks only sometimes, make it a separate component and render that component conditionally:{loggedIn && <Tabs />}is fine, because the hooks live insideTabs, which is either there with all its hooks or not there at all. - 6
Let a tool check it. The official ESLint plugin
eslint-plugin-react-hookshas areact-hooks/rules-of-hooksrule that finds every broken call in your editor before you even run the code, andreact-hooks/exhaustive-depsfor effect dependencies (Phase 7). Vite and Next.js templates include it. Version 7 adds React Compiler-powered rules (such asreact-hooks/purityandreact-hooks/set-state-in-render) to itsrecommendedpreset.
Explain it without notes
State the two Rules of Hooks.
Why does React need hooks to be called in the same order on every render?
How do you fix a hook that only needs to run in some situations?
What does eslint-plugin-react-hooks do?
Practice
This component breaks the rules. Rewrite it correctly:
``jsx
function Price({ amount }) {
if (amount === 0) return <p>Free</p>;
const [showTax, setShowTax] = useState(false);
return <p onClick={() => setShowTax(!showTax)}>{showTax ? amount * 1.18 : amount}</p>;
}
A Comments section should have its own useState for the new comment text, but only appear when open is true. Show the correct structure.
Trade-offs
- ↔
Calling a hook you don't always need costs almost nothing; splitting into a separate component is cleaner when the conditional part has several hooks or its own UI.
Done when you can
I can state both Rules of Hooks.
I can explain that hooks are matched by call order.
I can fix a conditional hook and a hook after an early return.
I can read React's hook-order table and the
Rendered more hookserror.I know to enable
eslint-plugin-react-hooksin every project.