react-hooks/rules-of-hooks

Require hooks to be called at the top level of a component or a custom hook

error by default

Also called react_hooks/rules-of-hooks, react/rules-of-hooks

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:

bunfig.toml
[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.

bunfig.toml
[lint.rules]
"react-hooks/rules-of-hooks" = ["error", {
  # additionalHooks = <string>,
}]

Examples#

example.jsx1 problem
function Profile({ user }) {
  if (user) {
    const [editing, setEditing] = useState(false);
  }
  return <p>{user?.name}</p>;
}
example.jsx1 problem
function Profile({ user }) {
  if (!user) {
    return null;
  }
  const [editing, setEditing] = useState(false);
  return <p onClick={() => setEditing(!editing)}>{user.name}</p>;
}
example.jsx1 problem
function List({ items }) {
  return items.map(item => {
    const id = useId();
    return <li key={id}>{item.name}</li>;
  });
}
example.js1 problem
function loadTheme() {
  return useContext(ThemeContext);
}
example.jsxNo problems
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:

example.jsxNo problems
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:

example.jsNo problems
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:

Button.stories.jsxNo problems
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:

example.jsx1 problem
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#

OptionWhat it does
additionalHooksAccepted, but this rule doesn't use it.