New
Introducing React Doctor for Enterprise

react-doctor/no-direct-state-mutation

Replace the mutation with a setter call that produces a new reference: `setItems([...items, newItem])`, `setItems(items.filter(x => x !== target))`, `setItems(items.toSorted(...))`. React only re-renders on a new reference, so in-place updates are silently dropped

  • Category: State & Effects
  • Severity: warn
  • Source: oxlint-plugin-react-doctor
  • Framework: global
  • Enabled when: always

Validation prompt

Use this to decide whether a fired diagnostic is real or a false positive.

Fires inside an uppercase-named function component when a useState value is the root of either an AssignmentExpression with a MemberExpression LHS (items.x = y, items[0] = y) or a CallExpression on a MUTATING_ARRAY_METHODS method (push, pop, shift, unshift, splice, sort, reverse, fill, copyWithin). Lexical shadowing by function params and inner var/let/const declarations is respected. False positive: the state binding name is reused in some outer scope the lightweight shadow tracker can't see (rare).

Fix prompt

Use this once validation confirms the diagnostic is real.

Replace each mutation with a setter call that produces a new reference: setItems([...items, newItem]) instead of items.push, setItems(items.filter(x => x !== target)) instead of splice, and the immutable methods toSorted / toReversed / toSpliced / with (or [...items].sort()) instead of in-place sort/reverse. React compares with Object.is and silently bails on a same-reference update. See https://react.dev/learn/updating-arrays-in-state

More State & Effects rules from the rules reference:

  • react-doctor/no-effect-chain: Compute as much as possible during render (e.g. `const isGameOver = round > 5`) and write all related state inside the event handler that originally fires the chain. Each effect link adds an extra render and makes the code rigid as requirements evolve
  • react-doctor/no-effect-event-handler: Move the conditional logic into onClick, onChange, or onSubmit handlers directly
  • react-doctor/no-effect-event-in-deps: Call the useEffectEvent callback inside the effect body without listing it; its identity is intentionally unstable
  • react-doctor/no-effect-with-fresh-deps: Move the constructed value into the hook body and depend on its primitive inputs, or memoize it with useMemo/useCallback so its reference is stable.
  • react-doctor/no-event-trigger-state: Delete the trigger state (`useState(null)` plus the `useEffect` that watches it) and call the side-effect (`post(...)` / `navigate(...)` / `track(...)`) directly inside the event handler that previously called the setter. State should not exist purely to schedule effect runs