ESC

Type to search the knowledge base.

Rules of Hooks

Why hooks must be top-level and only in React functions — call order, the linter, and the bugs that break when you cheat.

beginner5 min read
  • react
  • hooks
  • rules-of-hooks

Hooks look like normal functions. They are not. React associates each hook call with a slot in a fixed call order for that component instance. Break the order and React wires state, effects, and context to the wrong slots — silent wrong UI, not a helpful error every time.

The two rules are short. The why is what interviews and production bugs care about.

  1. Only call Hooks at the top level — not inside loops, conditions, or nested functions.
  2. Only call Hooks from React functions — function components or custom hooks (names starting with use).

Official source: Rules of Hooks.

The model: ordered slots, not names

On every render, React walks the component body and records hook calls in the order they appear. Internally it’s closer to “hook #0, hook #1, hook #2” than “the one named count.”

function Profile() {
  const [name, setName] = useState('');     // slot 0
  const [age, setAge] = useState(0);        // slot 1
  useEffect(() => { /* ... */ }, [name]);   // slot 2
  // ...
}

Next render, React expects the same number of hooks in the same order. Slot 0 still feeds name. Slot 1 still feeds age. The labels in your source code are for you; the runtime only trusts position.

Custom hooks just append more slots in the order they call hooks:

function useFormField(initial) {
  const [value, setValue] = useState(initial); // next slot for the caller
  const [error, setError] = useState(null);
  return { value, setValue, error, setError };
}

function Signup() {
  const email = useFormField('');  // uses slots 0–1
  const password = useFormField(''); // uses slots 2–3
  // ...
}

That composition is why hooks scale — and why conditional hook calls destroy the mapping.

What breaks if you call hooks conditionally

// ❌ Never do this
function Search({ enabled }) {
  if (!enabled) {
    return <p>Search off</p>;
  }
  const [query, setQuery] = useState(''); // sometimes slot 0, sometimes missing
  useEffect(() => {
    // fetch...
  }, [query]);
  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

First render with enabled === true: two hooks. Next render with enabled === false: zero hooks. React’s previous state for slot 0 no longer matches this tree. You get invariant errors in development, and in bad cases state from the wrong hook after a structural change.

Correct pattern: always call the hook; branch the behavior.

// ✅ Hooks always run; UI / effects branch
function Search({ enabled }) {
  const [query, setQuery] = useState('');

  useEffect(() => {
    if (!enabled || !query) return;
    // fetch...
  }, [enabled, query]);

  if (!enabled) return <p>Search off</p>;
  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

Same idea for early returns: put hooks above any return that depends on props/state.

Loops and nested functions

// ❌ Hooks inside map — count depends on list length
function List({ items }) {
  return items.map((item) => {
    const [selected, setSelected] = useState(false); // illegal
    return <Row key={item.id} selected={selected} />;
  });
}

Extract a child component. Each child instance gets its own hook list:

function Row({ item }) {
  const [selected, setSelected] = useState(false);
  return (
    <button type="button" aria-pressed={selected} onClick={() => setSelected((s) => !s)}>
      {item.label}
    </button>
  );
}

function List({ items }) {
  return items.map((item) => <Row key={item.id} item={item} />);
}

Nested event handlers and useMemo factories also must not call hooks — they can close over values from hooks declared at the top of the component.

Only in React functions

Calling useState from a plain utility, a class method, or a random event module has no component fiber to attach to:

// ❌
export function formatUser(user) {
  const [cache] = useState({}); // not a component or custom hook
  // ...
}

Allowed call sites:

Call site OK?
Function component body Yes
Custom hook (use*) body Yes
Class component No — use class state/lifecycle
Regular JS module / event handler / promise callback No

Custom hooks are how you share stateful logic without HOCs:

function useOnlineStatus() {
  const [online, setOnline] = useState(
    typeof navigator !== 'undefined' ? navigator.onLine : true
  );

  useEffect(() => {
    const on = () => setOnline(true);
    const off = () => setOnline(false);
    window.addEventListener('online', on);
    window.addEventListener('offline', off);
    return () => {
      window.removeEventListener('online', on);
      window.removeEventListener('offline', off);
    };
  }, []);

  return online;
}

The eslint plugin is not optional theater

eslint-plugin-react-hooks (rules-of-hooks + exhaustive-deps) catches almost every structural violation at edit time. Treat rule-of-hooks failures as blockers, not style nits.

If the linter flags a hook and your first instinct is // eslint-disable, stop. Restructure: child component, always-run hook + conditional body, or move non-React logic out of the hook call path.

Footguns that still sneak through

  • Conditional custom hooks — if (x) useThing() is the same bug as conditional useState.
  • Dynamic hook lists — “call N hooks for N filters” belongs in N components or one structure that always has a fixed max.
  • Copy-pasting hooks below an early return after a refactor — easy regression; keep a consistent “hooks block then UI” layout.
  • Thinking the rule is about purity only — purity matters for concurrent rendering, but the order rule is specifically about identity of state across renders.

Interview angle

Question: “Why can’t you call hooks inside if?”

Strong answer: React stores hook state in a list tied to call order for that fiber. Conditional calls change the length/order of the list between renders, so later hooks read the wrong memory. Fix by always calling hooks and branching inside them or in JSX after the hooks block.

Follow-ups you should own:

  • How custom hooks compose (slots concatenate)
  • Why index keys + remounting differ from hook rules (related identity topics — see Reconciliation)
  • Difference between rules-of-hooks and exhaustive-deps

Further reading

Related guides