React: Hook Called Conditionally

ID

javascript.react_hooks_correctness

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

JavaScript

Tags

hooks, react, reliability

Description

Reports calls to a React Hook — useState, useEffect, useMemo, useCallback, useRef, useContext, and any other identifier matching the use[A-Z][A-Za-z0-9]* convention — placed inside a conditional (if, ternary, switch), a loop, or a nested non-hook function. React’s reconciler relies on hooks being called in the same order on every render; a conditional or loop may skip or reorder them, corrupting the per-component hook state slot map.

Rationale

// Bad — `useState` only runs when `cond` is true; render-to-render, the
// hook order changes, and every other hook in this component gets the
// wrong state slot.
function Bad({ cond }) {
  if (cond) {
    const [v, setV] = useState(0);
    return <span>{v}</span>;
  }
  return null;
}

// Bad — same hazard via ternary.
function BadTernary({ cond }) {
  const v = cond ? useState(0) : null;
  return <span>{v}</span>;
}

// Bad — `useEffect` per loop iteration.
function BadLoop({ items }) {
  for (const item of items) {
    useEffect(() => log(item), [item]);
  }
}

Remediation

Always call hooks at the top of the function body. Move conditional logic into the hook (its callback / its dependency list) rather than around the hook call:

// Good — hook always runs; the effect's body decides what to do.
function Good({ cond }) {
  const [v, setV] = useState(0);
  useEffect(() => {
    if (!cond) return;
    log(v);
  }, [cond, v]);
  return <span>{v}</span>;
}