react-doctor/rules-of-hooks
Enforce the Rules of Hooks: call hooks only at the top level of components and custom hooks, never in conditions or loops.
- Status
- Active
- Category
- Correctness
- Assessment
- Evidence-required risk
- Required evidence
- source code
- Default configuration
- Enabled
- Default severity
- error
Show technical metadata
- Scope
- All supported frameworks
- Active when
- always (unless customRulesOnly=true)
- Requirements
- react
- Tags
- test-noise
- Priority
- 89 (P0)
- Source
- oxlint-plugin-react-doctor
- Rule set
- oxlint-plugin-react-doctor 0.9.3 (prompt schema 2)
- Documentation
- Official documentation
Validation prompt
Confirm the detector match and collect the required evidence before deciding whether an edit is warranted.
Fires when a use-prefixed call (or 'Namespace.useFoo') runs somewhere a hook is illegal: (a) the enclosing function's name is neither PascalCase nor use-prefixed ('non-component/hook' message), or (b) inside a real component/'use*' hook it sits in an if/loop/try/catch/'&&'/ternary/after an early return/inside an async fn ('called conditionally|in a loop|inside try|async' message). CONFIRM only case (b) where the callee is a genuine React hook and the file ships to production. SUPPRESS case (a) when the enclosing function actually renders as a component despite its name: a Storybook 'render:'/'decorators:'/'play:' callback or any '*.stories.*' file, a JSX render-prop ('placeholder: () => <.../>'), or a component whose identifier merely isn't uppercase ('function _Chat()'). SUPPRESS any callee that is not a real React hook but only borrows the 'use' prefix: a store/atom/util factory ('useAuthQuery' returning a nanostores atom, 'useWizard', a renamed helper) called from a plain non-render function; rename it to drop the collision. Also suppress when the diagnostic targets test/story/playground/example code (test-noise).
Evidence boundary
The diagnostic proves only that the detector’s modeled source pattern matched. It does not prove runtime impact, product intent, rendered failure, or that one remediation is correct.
Establish the environment, repository policy, exceptions, and required rendered or runtime evidence before deciding the occurrence.
Record one outcome:
- Confirmed failure: The required evidence establishes the violation.
- Rejected: A documented exception or false-positive predicate applies.
- Needs evidence: Named evidence can still be collected.
- Unavailable: Required evidence cannot be collected in this run.
- Waived with evidence: An authorized, scoped exception applies to an established failure.
- Observation: The review records an optional tradeoff without claiming a defect.
A waiver records its scope, authority, evidence, and review condition. It is not a pass or false positive.
Default severity is registry metadata. Use the occurrence’s JSON severity after repository configuration when ordering real findings.
Fix prompt
Apply this candidate correction only after the required evidence confirms the risk.
First confirm the callee is a real React hook AND the site is a real render path; the principle is 'every render must hit the same hook calls in the same order'. For genuine violations: (A) Conditional/early-return: hoist the hook above all branches and move the condition inside it: 'const ctx = useContext(C); if (!ctx) return {};' not 'if(!id) return; const ctx=useContext(C);'; for effects, 'useEffect(()=>{ if(cond) run(); },[cond])'. (B) Per-item hook in a loop/.map: extract a child component so each item owns its own state, never call hooks in the loop body. (C) Branching dispatcher (e.g. overload picking 'useXImpl(a)' vs 'useXImpl(b)'): call the hook once unconditionally, branch only its arguments. For the common FALSE alarms, the fix is NOT to restructure: if the function already renders (Storybook render/decorator, render-prop) leave it; if the name is just non-uppercase, rename to PascalCase ('_Chat'->'ChatBody'); if the callee isn't a React hook, rename it off the 'use' prefix ('useAuthQuery'->'createAuthQuery'). Do NOT mechanically wrap the hook in a new 'useX' wrapper or hoist a non-hook call. See https://oxc.rs/docs/guide/usage/linter/rules/react/rules-of-hooks and https://react.dev/reference/rules/rules-of-hooks
Repository-wide copy prompt
Use this repository-wide prompt only after validating each occurrence. For one occurrence, use the guidance above.
Show repository-wide prompt
Fix every confirmed react-doctor/rules-of-hooks diagnostic in the current repository.
Required change:
- First confirm the callee is a real React hook AND the site is a real render path; the principle is 'every render must hit the same hook calls in the same order'. For genuine violations: (A) Conditional/early-return: hoist the hook above all branches and move the condition inside it: 'const ctx = useContext(C); if (!ctx) return {};' not 'if(!id) return; const ctx=useContext(C);'; for effects, 'useEffect(()=>{ if(cond) run(); },[cond])'. (B) Per-item hook in a loop/.map: extract a child component so each item owns its own state, never call hooks in the loop body. (C) Branching dispatcher (e.g. overload picking 'useXImpl(a)' vs 'useXImpl(b)'): call the hook once unconditionally, branch only its arguments. For the common FALSE alarms, the fix is NOT to restructure: if the function already renders (Storybook render/decorator, render-prop) leave it; if the name is just non-uppercase, rename to PascalCase ('_Chat'->'ChatBody'); if the callee isn't a React hook, rename it off the 'use' prefix ('useAuthQuery'->'createAuthQuery'). Do NOT mechanically wrap the hook in a new 'useX' wrapper or hoist a non-hook call. See https://oxc.rs/docs/guide/usage/linter/rules/react/rules-of-hooks and https://react.dev/reference/rules/rules-of-hooks.
Validation before editing:
Fires when a use-prefixed call (or 'Namespace.useFoo') runs somewhere a hook is illegal: (a) the enclosing function's name is neither PascalCase nor use-prefixed ('non-component/hook' message), or (b) inside a real component/'use*' hook it sits in an if/loop/try/catch/'&&'/ternary/after an early return/inside an async fn ('called conditionally|in a loop|inside try|async' message). CONFIRM only case (b) where the callee is a genuine React hook and the file ships to production. SUPPRESS case (a) when the enclosing function actually renders as a component despite its name: a Storybook 'render:'/'decorators:'/'play:' callback or any '*.stories.*' file, a JSX render-prop ('placeholder: () => <.../>'), or a component whose identifier merely isn't uppercase ('function _Chat()'). SUPPRESS any callee that is not a real React hook but only borrows the 'use' prefix: a store/atom/util factory ('useAuthQuery' returning a nanostores atom, 'useWizard', a renamed helper) called from a plain non-render function; rename it to drop the collision. Also suppress when the diagnostic targets test/story/playground/example code (test-noise).
Constraints:
- Make the smallest change that fixes the root cause.
- Preserve behavior and interfaces unrelated to this diagnostic.
- Reuse existing project components, utilities, and conventions.
- Do not introduce render-phase side effects, render-phase state updates, or Hooks rule violations.
- Adapt identifiers and framework details instead of copying blindly.
- Do not disable the rule or suppress matching code.
- Confirm this rule is enabled for the project:
always (unless customRulesOnly=true).
Assessment:
- Record detector evidence, applicability facts, assumptions, missing evidence, and the rule class for this occurrence.
- Return one outcome: Confirmed failure, Rejected, Needs evidence, Unavailable, Waived with evidence, or Observation.
- A waiver records the established failure, scope, authority, evidence, and review or expiry condition. It is not a pass or false positive.
Verification:
- Run focused tests for the changed behavior.
- Run React Doctor and confirm this diagnostic no longer appears from changed code.
- Run an unfiltered scan of the affected scope before claiming no cross-category regression.
- Report the files changed and any checks you could not run.