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>;
}