Files
Skosh 17eeeb5367 fix(rules): recognize member hook state consumption (#1717)
* fix(rerender-state-only-in-handlers): recognize state consumed via member expression hook calls

State consumed by hook calls through member expressions (e.g. styles.useVariants({ state }))
was incorrectly flagged as unused. This commonly occurs with react-native-unistyles,
where state selects style variants via StyleSheet.useVariants().

The rule now checks both direct identifier calls (useX) and member expression calls
(obj.useX) when collecting custom hook arguments, following the existing pattern used
throughout the codebase.

Fixes #1716

Co-authored-by: Skosh <skoshx@users.noreply.github.com>

* chore: add changeset for member expression hook fix

Co-authored-by: Skosh <skoshx@users.noreply.github.com>

* test: add edge case for non-hook member expressions

Co-authored-by: Skosh <skoshx@users.noreply.github.com>

* fix(rules): preserve state setter bindings

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
Co-authored-by: Aiden Bai <aiden@million.dev>
2026-08-29 22:13:41 -07:00
..
2026-08-13 04:38:53 -07:00

oxlint-plugin-react-doctor

version downloads

oxlint plugin for React Doctor. Diagnoses React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues.

This package owns the rule implementations (287 rules across architecture, performance, correctness, security, accessibility, bundle-size, framework-specific, react-builtins, and a11y buckets). eslint-plugin-react-doctor wraps these same rules for ESLint, and the full diagnostic CLI lives in react-doctor.

Ported OXC react + jsx-a11y rules

The react-builtins/ and a11y/ buckets contain 100 rules ported from oxc-project/oxc's crates/oxc_linter/src/rules/{react,react_perf,jsx_a11y}/. They cover every rule React Doctor previously consumed via oxlint's built-in react/* and jsx-a11y/* plugins (now sourced natively as react-doctor/*), including react/rules-of-hooks and react/exhaustive-deps, which run on top of a TS port of OXC's scope analysis + control-flow-graph layer.

Install

npm install --save-dev oxlint oxlint-plugin-react-doctor
pnpm add -D oxlint oxlint-plugin-react-doctor
yarn add -D oxlint oxlint-plugin-react-doctor

Usage

In .oxlintrc.json:

{
  "jsPlugins": [{ "name": "react-doctor", "specifier": "oxlint-plugin-react-doctor" }],
  "rules": {
    "react-doctor/no-fetch-in-effect": "warn",
    "react-doctor/no-derived-state-effect": "warn",
  },
}

Run oxlint as normal:

npx oxlint .

Lightweight engine entry

oxlint-plugin-react-doctor/core exposes rule metadata, cross-file collectors, and project scan rules without loading the full oxlint visitor registry. It is intended for engine integrations. Oxlint configurations should continue using the package's root entry.

Available rules

The full rule list lives in rule-registry.ts. Rules exported by this package use the react-doctor/* namespace.

Choose a runner for each diagnostic

Standalone Oxlint runs rules that inspect one source file. The React Doctor CLI also runs whole-project and package analyzers.

Diagnostic source Standalone Oxlint Runner
react-doctor/* rules with per-file visitors Yes Configure them under rules as shown above
Project rules such as react-doctor/circular-dependency and react-doctor/unused-dependency No Run the React Doctor CLI
deslop/* diagnostics No Run the React Doctor CLI
socket/* diagnostics No Run the React Doctor CLI
react-hooks-js/* React Compiler diagnostics Separate plugin Register eslint-plugin-react-hooks or run the React Doctor CLI

The deslop/* names are CLI output aliases for project rules. These rules need the complete dependency graph, so their plugin entries have no per-file visitors. The socket/* diagnostics need package and supply-chain data. Neither prefix names an Oxlint plugin exported by this package.

Rules in the security-scan bucket also require a project-wide file scan. They register metadata here but do nothing under standalone Oxlint or ESLint. The React Doctor CLI executes them during its whole-tree scan.

Run React Compiler diagnostics in standalone Oxlint

React Doctor loads React Compiler diagnostics from eslint-plugin-react-hooks when it detects the compiler. To use them in standalone Oxlint, install and register that plugin:

npm install --save-dev eslint-plugin-react-hooks

Add both plugins to .oxlintrc.json. The react-compiler capability enables compiler-specific React Doctor cleanup rules:

{
  "jsPlugins": [
    { "name": "react-doctor", "specifier": "oxlint-plugin-react-doctor" },
    { "name": "react-hooks-js", "specifier": "eslint-plugin-react-hooks" },
  ],
  "settings": {
    "react-doctor": {
      "capabilities": ["react-compiler"],
    },
  },
  "rules": {
    "react-doctor/react-compiler-no-manual-memoization": "warn",
    "react-hooks-js/immutability": "error",
    "react-hooks-js/refs": "error",
  },
}

The react-hooks-js namespace is the alias React Doctor uses for the external plugin. It is not exported by oxlint-plugin-react-doctor.

Each rule can be set to "error", "warn", or "off":

{
  "rules": {
    "react-doctor/effect-needs-cleanup": "error",
    "react-doctor/no-array-index-as-key": "warn",
  },
}

"You Might Not Need an Effect" rule family

Eight rules ported 1:1 from eslint-plugin-react-you-might-not-need-an-effect (MIT, NickvanDyke) ship natively in this package — same rule IDs, same diagnostic messages, same semantics (195 of 196 upstream test cases pass; the remaining one is upstream's own todo: true). Attribution and known divergences live in SOURCE.md.

Rule What it catches
react-doctor/no-derived-state Storing derived state via useEffect instead of computing during render
react-doctor/no-chain-state-updates Chaining state updates across effects
react-doctor/no-event-handler Using state + a guarded effect as an event handler
react-doctor/no-adjust-state-on-prop-change Adjusting state in an effect when a prop changes
react-doctor/no-reset-all-state-on-prop-change Resetting all state in an effect (use a key prop instead)
react-doctor/no-pass-live-state-to-parent Pushing live state to a parent via a callback in an effect
react-doctor/no-pass-data-to-parent Passing fetched data to a parent via a callback in an effect
react-doctor/no-initialize-state Initializing state inside a mount-only effect (pass it to useState instead)

If you previously enabled them as effect/* via the optional peer dep, drop the peer dep — they're enabled by default through React Doctor's CLI config now.

Want the CLI too?

This package only ships the oxlint plugin. To run React Doctor's full scan (with scoring, JSON reports, agent integration, etc.), use the main CLI:

npx react-doctor@latest

See the React Doctor README for the full feature set.

License

MIT