logoESLint React

immutability

Validates against mutating props, state, and other immutable values, including through functions passed into frozen contexts such as JSX props, hook arguments, and hook return values.

This is an evaluation implementation and may contain false positives or negatives that have not yet been fully audited. Review each report carefully before applying fixes.

Full Name in eslint-plugin-react-x

react-x/immutability

Full Name in @eslint-react/eslint-plugin

@eslint-react/immutability

Features

🧪

Rule Details

This rule validates that values React treats as immutable are not mutated, in two ways:

Direct mutations. A member assignment, update, or deletion (x.foo = v, x[0]++, delete x.foo) or a mutating method call (x.push(1)) is flagged when the mutated value's root identifier resolves — through variable-declarator aliases — to:

  • A component's props (including destructured props)
  • A state value returned from useState, useReducer, or a custom hook matching the additionalStateHooks setting
  • A shallow copy of either (const copy = { ...state } / const copy = [...state]), when the mutation reaches a nested value: the copy's own top-level slots are new, but nested objects and arrays are still shared with the original

Functions reaching frozen contexts. A function that mutates a variable captured from an enclosing scope is, in effect, a mutable value: unlike a mutable array or object, the caller of the function has no way to prevent the mutation from happening once the function is invoked. This rule flags such functions when they are handed to a context that expects a stable, "frozen" value:

  • Passed as a JSX prop
  • Passed as an argument to a hook call
  • Returned from a custom hook

Mutations to ref-like values (an identifier named ref or ending in Ref) are exempted, since refs are mutable by design.

Examples

// 🔴 Problem: `handleChange` mutates the state value in place
function Example({ initial }) {
  const [values, setValues] = useState(initial);
  const handleChange = (itemId, diff) => {
    values[itemId].confirmedQuantity = diff;
    setValues(values);
  };
}
// 🔴 Problem: a shallow copy still shares nested values with the original state
function Example({ initial }) {
  const [values, setValues] = useState(initial);
  const handleChange = (itemId, diff) => {
    const copyValues = { ...values };
    copyValues[itemId].confirmedQuantity = diff; // copyValues[itemId] === values[itemId]
    setValues(copyValues);
  };
}
// 🟢 Recommended: copy every level along the mutated path
function Example({ initial }) {
  const [values, setValues] = useState(initial);
  const handleChange = (itemId, diff) => {
    setValues({
      ...values,
      [itemId]: { ...values[itemId], confirmedQuantity: diff },
    });
  };
}
// 🔴 Problem: `fn` mutates `cache` after render
function Component() {
  const cache = new Map();
  const fn = () => {
    cache.set("key", "value");
  };
  return <Foo fn={fn} />;
}
// 🟢 Recommended: use state instead of a captured mutable variable
function Component() {
  const [cache, setCache] = useState(() => new Map());
  const fn = () => {
    setCache((prev) => new Map(prev).set("key", "value"));
  };
  return <Foo fn={fn} />;
}

Options

Shared Settings

Custom ref hooks can also be configured via shared ESLint settings, which apply consistently across all rules in the plugin:

{
  "settings": {
    "react-x": {
      "additionalRefHooks": "(useMyRef|useCustomRef)"
    }
  }
}

Custom state hooks whose returned values should be treated as immutable state can be configured the same way:

{
  "settings": {
    "react-x": {
      "additionalStateHooks": "(useMyState|useCustomState)"
    }
  }
}

Versions

Resources

Further Reading


See Also

  • react-x/purity
    Validates that components and hooks are pure by checking that they do not call known-impure functions during render.

On this page