The rules of hooks are two: call hooks only at the top level of your component, never inside a condition, a loop, a nested function or after an early return; and call them only from function components or from custom hooks. They exist because React tells hooks apart by the order you call them in, so that order must be the same on every render.
React stores the values of a component's hooks in a list attached to that component. On each render it walks the list: the first hook call gets the first slot, the second call the second slot, and so on. There are no names, only positions. The example below is a toy version of that idea in a few lines of plain JavaScript, not React's own code, so you can break it safely.
The first render fills three slots: Ada, Countess, 36. Click "Render without nickname": the conditional call is skipped, so the age call takes slot 1 and reads Countess. Real React notices the mismatch in some cases and throws, and in others hands the wrong value to the wrong hook, which is worse. Move the toyUseState('Countess') call above the if and both buttons give the right age.
The two rules in code
function Profile({ user }) {
// Good: always called, always in this order
const [tab, setTab] = useState('posts');
const theme = useContext(ThemeContext);
useEffect(() => {
document.title = user.name;
}, [user.name]);
// Bad: inside a condition, a loop or a nested function
if (user.isAdmin) {
const [log, setLog] = useState([]); // breaks the order
}
}
A hook call that is always reached, in the same order, is fine wherever it sits in the body, as long as it comes before any early return.
Fix a hook called conditionally
The linter reports this as React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render. The fix is always the same: call the hook every time, and put the condition inside it.
// Broken: the effect exists on some renders and not others
function Notifications({ user }) {
if (user) {
useEffect(() => {
console.log('subscribe for', user.name);
}, [user]);
}
return null;
}
The effect is declared on every render, and the if inside it decides whether to do anything. Log in and out a few times and the Console shows a subscribe for each login and an unsubscribe for each logout.
Hooks after an early return
An early return is a condition too. This produces "Rendered more hooks than during the previous render" the first time loading turns false, because that render reaches a hook the previous one never called. When a render calls fewer hooks than the previous one, the message is "Rendered fewer hooks than expected".
// Broken: useState is skipped while loading
function Comments({ loading, comments }) {
if (loading) return <p>Loading...</p>;
const [expanded, setExpanded] = useState(false);
return <List items={comments} expanded={expanded} />;
}
Every hook sits above the return, so both branches call the same hooks. Click "Toggle loading", expand the list, then toggle loading twice: the list remembers it was expanded, because the state was never skipped.
Hooks in a loop
You cannot call useState once per item in a loop, because the number of items changes. Give each item its own component and call the hook there; each instance has its own slots.
"Invalid hook call"
The message starts with "Invalid hook call. Hooks can only be called inside of the body of a function component." It has three usual causes:
- The hook runs while React is not rendering a function component. In an event handler, inside an effect callback, in a class component, or at module level. Move it into the component body or a custom hook, and call the custom hook from the component.
reactandreact-domversions do not match. Install the same version of both.- There are two copies of React. A linked local package or a library that bundles its own React brings a second copy, and hooks from one copy do not work with the renderer of the other. Run
npm ls reactand make sure only one version appears.
npm ls react
The preview here runs like a production build, which does not print these messages: a hook called from an event handler fails with a plain TypeError such as "Cannot read properties of null", and React's own errors arrive as short "Minified React error" codes. Run your app in development mode to read the full text.
Let the linter enforce it
eslint-plugin-react-hooks checks both rules as you type, and also checks effect dependency arrays. Vite's React template and Next.js's ESLint config include it. In a flat ESLint config, turn on its recommended preset:
// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
import { defineConfig } from 'eslint/config';
export default defineConfig([reactHooks.configs.flat.recommended]);
The two core rules are react-hooks/rules-of-hooks (an error) and react-hooks/exhaustive-deps (a warning). Since version 7 the preset also turns on rules that come from React Compiler, such as react-hooks/purity and react-hooks/refs, which flag impure renders and reading refs during render. To pick rules one by one, register the plugin as plugins: { 'react-hooks': reactHooks } and list the rules you want.
The plugin finds hooks by name: any function whose name starts with use followed by a capital letter is treated as a hook, and only components (capitalized names) and other use functions may call it. That is why custom hooks must use the prefix.
The exception: use
use, added in React 19, reads a promise or a context, and unlike every other hook it may be called inside a condition or a loop. It still must be called during render, inside a component or a hook.
Only the themed heading changes color when you switch the theme. Try the same thing with useContext inside the if and the linter flags it. The use hook page covers reading promises with it.
Frequently Asked Questions
What are the rules of hooks?
Two rules. Call hooks only at the top level of a function component or custom hook, never inside conditions, loops, nested functions or after an early return. And call hooks only from function components or custom hooks, not from regular functions or class components.
Why can't hooks be called conditionally?
React does not know your hooks by name. It stores their values in a list and hands them back in the order the calls happen. If a condition skips a call, every later hook reads the value that belonged to the hook before it.
What does "Rendered more hooks than during the previous render" mean?
A render called more hooks than the render before it, usually because a hook sits inside an if or after an early return that ran differently this time. Move every hook above the condition and put the condition inside the hook instead.
How do I fix "Invalid hook call"?
Check three things: the hook runs while React renders a function component (not in an event handler, an effect callback, a class component or at module level), react and react-dom have matching versions, and the app has only one copy of React (npm ls react lists them).
Can the use hook be called conditionally?
Yes. use (React 19) is the one exception: you can call it inside if blocks and loops. It still has to be called inside a component or a hook, not in an event handler.