React Hook Called Conditionally

ID

javascript.react_usestate_direct

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

JavaScript

Tags

react, reliability, rules-of-hooks

Description

Reports a React hook (useState, useEffect, …​ or any custom hook named useXxx) that is reached only after a conditional, loop, or short-circuit operator:

function Counter({ enabled }) {
  if (enabled) {
    const [n, setN] = useState(0);   // FLAW — conditional hook
  }
  return null;
}

Rationale

React relies on the call order of hooks within a component to decide which slot of internal state each hook owns. If the same component sometimes calls four hooks and sometimes calls three, React’s slot mapping shifts between renders and every later hook reads the wrong state. The result is corruption that is very hard to debug.

The Rules of Hooks state two invariants:

  1. Only call hooks at the top level of a component (or another hook).

  2. Don’t call hooks inside loops, conditions, or nested helpers.

Remediation

Move the hook call to the top of the component, then branch on the value:

function Counter({ enabled }) {
  const [n, setN] = useState(0);
  if (!enabled) return null;
  // ... use n / setN here
}

If a hook genuinely needs to be skipped, do that at a higher level — render a different component instead.

Notes

The rule recognises any identifier that starts with use followed by an upper-case letter (useState, useFooBar, …​) as a hook. Identifiers like usefoo or usage are not hooks under React’s convention and are not reported.

The rule walks the parent chain from the call up to the enclosing function. Crossing an if (or its else), a loop, a switch, a try/catch, a ternary, or a &&/|| makes the call conditional. A nested function stops the walk because the inner function is its own scope (and would be its own component / custom hook to satisfy).