Linter · Rules
react-hooks/rules-of-hooks
Require hooks to be called at the top level of a component or a custom hook
React tells one useState from the next by the order you call them in. If a hook runs on one render but not the next, every hook after it gets another hook's state, and React throws "Rendered fewer hooks than expected".
So you must call every hook on every render, in the same order. You can't call one in a condition, a loop, a nested function, or after an early return. You also can't call one in a class, an async function, at the top level of a file, or in a function that's neither a component nor a hook. A name counts as a hook if it's use or starts with use and a capital letter. A component's name starts with a capital letter.
This rule is on by default if your package.json depends on react.
Configure
On by default as an error. To turn it off:
[lint.rules]
"react-hooks/rules-of-hooks" = "off" # or "warn"
Every option, set to its default. The ones that are commented out are unset by default.
[lint.rules]
"react-hooks/rules-of-hooks" = ["error", {
# additionalHooks = <string>,
}]
Examples
Reported:
function Profile({ user }) {
if (user) {
const [editing, setEditing] = useState(false);
}
return <p>{user?.name}</p>;
}
function Profile({ user }) {
if (!user) {
return null;
}
const [editing, setEditing] = useState(false);
return <p onClick={() => setEditing(!editing)}>{user.name}</p>;
}
function List({ items }) {
return items.map(item => {
const id = useId();
return <li key={id}>{item.name}</li>;
});
}
function loadTheme() {
return useContext(ThemeContext);
}
Not reported:
function Profile({ user }) {
const [editing, setEditing] = useState(false);
if (!user) {
return null;
}
return <p onClick={() => setEditing(!editing)}>{user.name}</p>;
}
function useTheme() {
return useContext(ThemeContext);
}
use is the exception. You can call it in a condition or a loop, but not in a try block:
function Note({ notePromise }) {
if (!notePromise) {
return null;
}
const note = use(notePromise);
return <p>{note.text}</p>;
}
A use that the file declares itself isn't React's, unless it's imported from "react". Playwright passes a function with that name to every fixture:
import { test as base } from "@playwright/test";
export const test = base.extend({
user: async ({ page }, use) => {
await page.goto("/login");
await use("alice");
},
});
In a *.stories.* file, a function named render counts as a component, because Storybook renders it as one:
export const Controlled = {
render: args => {
const [on, setOn] = useState(false);
return <Button {...args} active={on} onClick={() => setOn(!on)} />;
},
};
With an ESLint config file both are reported, as eslint-plugin-react-hooks does.
You can only call a function made with useEffectEvent from an effect or another effect event in the same component:
function Chat({ room }) {
const onConnected = useEffectEvent(() => {
notify(room);
});
return <button onClick={onConnected}>Connect</button>;
}
Your own effect hooks count as effects if the additionalEffectHooks setting is a regular expression that matches their names. It goes under [lint.settings.react-hooks] in bunfig.toml, or under settings["react-hooks"] in an ESLint config.
Options
| Option | What it does |
|---|---|
additionalHooks | Accepted, but this rule doesn't use it. |