Skip to main content
interlace
Plugin: react-featuresRules

no-direct-mutation-state

no-direct-mutation-state rule

Prevent direct mutation of this.state in React class components. This rule is part of eslint-plugin-react-features.

Quick Summary

AspectDetails
SeverityError (correctness)
Auto-Fix❌ No (requires setState)
CategoryReact
ESLint MCP✅ Optimized for ESLint MCP integration
Best ForReact class components

Rule Details

Directly mutating this.state bypasses React's update mechanism, causing bugs and missing renders.

Why This Matters

IssueImpactSolution
🔄 No re-renderUI doesn't updateUse setState()
🐛 State inconsistencyBatched updates breakImmutable updates
🔍 Debug difficultyState changes not trackedProper state flow

Examples

❌ Incorrect

class Counter extends React.Component {
  handleClick = () => {
    this.state.count = this.state.count + 1;  // Direct mutation!
    this.state.items.push(newItem);  // Mutating array
  }
}

✅ Correct

class Counter extends React.Component {
  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
    this.setState(prev => ({
      items: [...prev.items, newItem]  // Immutable update
    }));
  }
}

// Or use hooks (recommended)
function Counter() {
  const [count, setCount] = useState(0);
  const handleClick = () => setCount(c => c + 1);
}

Configuration Examples

Basic Usage

{
  rules: {
    'react-features/no-direct-mutation-state': 'error'
  }
}

Further Reading

Known False Negatives

  • Aliases: const { items } = this.state; items.push(x) or const s = this.state; s.count = 1 mutate state through a local name and are not seen.
  • Mutation through a helper: passing this.state.items to a function that mutates it is not followed.
  • Mutating calls are recognised by method name (push, splice, sort, …) on a value read from this.state.

Not reported by design: any write in the constructor (where the initial state is set), and writes in a class that is not a React component.

Use this rule

npm install -D oxlint-plugin-react-features
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-react-features"],  "rules": {    "react-features/no-direct-mutation-state": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-react-features'],    rules: {      'react-features/no-direct-mutation-state': 'error',    },  },});

oxlint-plugin-react-features re-exports eslint-plugin-react-features/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.