React

The rules of hooks, with the cases that break

Call hooks only at the top level of a component or another hook. If the order changes between renders, React reads the wrong state slot.

Key points

  • The rules of hooks are about the order of calls, not the name of the function.
  • A call skipped by an early return shifts every later hook by one slot.
  • The official ESLint plugin ships under the MIT licence and flags the three common cases.

The rules of hooks are two: call them only at the top level of a component, and only inside React functions. Conditions, loops and nested callbacks break the order React builds state on.

Why the order matters

React does not store state by name. Each component carries an ordered list of slots: the first useState takes slot zero, the second takes slot one, and on the next render React reads the list in the same sequence. When a condition skips one call, every hook after it slides by one position and reads another hook's value.

The case that breaks most often

An early return above a hook is the most common way to break the rule without noticing: the first render never reaches the call, the second one does.

function Profile({ user, loading }) {
  const [open, setOpen] = useState(false);

  if (loading) return Loading;      // first render stops here

  const [tab, setTab] = useState("info");  // slot 1 appears only later

  return (
    
       setOpen(!open)}>{user.name}
      {open && }
    
  );
}

Move the return below every hook, or extract the conditional part into a child component: the child mounts only when you need it, and its hooks start with it, in order.

The other three cases

  • Inside a loop: a hook in a map runs as many times as the data dictates, so the list of slots changes with it.
  • Inside a callback: a function passed to onClick is not a component, and React has nowhere to put the slot.
  • Inside a class: hooks do not work in methods, so those components stay on this.state or get rewritten as functions.

The automatic check

The official ESLint plugin recognises the three cases and reports them during lint. Install it as a development dependency and enable it in a flat config:

npm install --save-dev eslint eslint-plugin-react-hooks
import reactHooks from "eslint-plugin-react-hooks";

export default [
  {
    files: ["src/**/*.{js,jsx,ts,tsx}"],
    plugins: { "react-hooks": reactHooks },
    rules: reactHooks.configs.recommended.rules,
  },
];

With eslint.config.js at the root, npx eslint src blocks the merge when someone puts a hook back inside an if. The plugin ships under the MIT licence, like React.

The same rules inside a custom hook

A custom hook is a function that starts with use and calls other hooks: the rules hold unchanged, because React does not see the function boundary. The prefix is not decoration, it is the signal the linter uses to know that the function may contain hooks.

That is why a custom hook never goes inside an if in the component that calls it: the condition belongs inside the hook, where the caller does not have to know about it.

Live records in the catalogue

Every React component in the catalogue uses hooks in its own files: list-item for a list row, expandable-tabs for tab state. Both are published records among the 111,985 components and the 1,556 blocks.

The limitation

The rules guarantee the order of calls, not the correctness of the dependencies: a useEffect with the wrong array passes lint and keeps reading stale values. That check is a separate rule and stays a warning, not a proof.

Where to continue

The difference between the two effects is in useEffect vs useLayoutEffect; what changes in version 19 is in React 19; the comparison with the other widely used framework is in Vue 3 vs React.

Frequently asked questions

Why does React require a fixed order for hooks?

Because state lives in a list of slots per component, not in named keys. React reads that list in call order: when the order changes, each hook reads another slot's value.

Can I call a hook inside an if that never changes?

No. The rule is static and the linter cannot prove the condition stays stable: the code works until someone edits the condition, and then it breaks in production.

Must a custom hook start with use?

Yes, that is the convention the linter uses to recognise a function that may contain hooks. Without the prefix the plugin skips the body and callers get no warning.

Do the rules apply to useMemo and useCallback?

Yes. Every hook consumes a slot, so they apply to all of them: useState, useEffect, useMemo, useCallback, useRef and custom hooks built on top.

What happens when I break the rules?

In clear cases React throws, usually “Rendered more hooks than during the previous render”. In ambiguous cases it throws nothing and shows wrong state, which is the worst failure to diagnose.