immutability
Validates against mutating props, state, and other values that are immutable.
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/immutabilityFull Name in @eslint-react/eslint-plugin
@eslint-react/immutabilityFeatures
🧪
Rule Details
A component's props and state are immutable snapshots. Never mutate them directly. Instead, pass new props down, and use the setter function from useState with a freshly created value.
Mutations are invisible to React — because the object or array reference hasn't changed, React has no way to know that something changed. As a result, the UI won't re-render, and your app will show stale data.
Examples
Adding items to an array
Array methods like push() mutate the original array in place. React cannot detect this change because the array reference remains the same.
// Problem: Mutating the array directly
function Component() {
const [items, setItems] = useState([1, 2, 3]);
const addItem = () => {
items.push(4); // Directly mutating the original array
// ^^^ Do not call 'push()' on 'items'. Props and state are immutable — create a new array instead.
setItems(items); // Same reference, won't trigger a re-render
};
}// Recommended: Create a new array
function Component() {
const [items, setItems] = useState([1, 2, 3]);
const addItem = () => {
setItems([...items, 4]); // New array
};
}Updating object properties
Directly assigning to an object's property mutates the existing object. React compares object references, not their contents, so the update is ignored.
// Problem: Mutating object properties directly
function Component() {
const [user, setUser] = useState({ name: "Alice" });
const updateName = () => {
user.name = "Bob"; // Directly mutating the original object
// ^^^ Do not mutate 'user' directly. Props and state are immutable — create a new object instead.
setUser(user); // Same reference
};
}// Recommended: Create a new object
function Component() {
const [user, setUser] = useState({ name: "Alice" });
const updateName = () => {
setUser({ ...user, name: "Bob" }); // New object
};
}Sorting or reversing lists
Array.prototype.sort() and Array.prototype.reverse() mutate the array in place. If you call them directly on state, you mutate React's internal snapshot.
// Problem: sort mutates the original array
function Component() {
const [items, setItems] = useState([3, 1, 2]);
const sortItems = () => {
setItems(items.sort()); // sort mutates the original array
};
}// Recommended: Spread into a new array before sorting
function Component() {
const [items, setItems] = useState([3, 1, 2]);
const sortItems = () => {
setItems([...items].sort()); // New array, then sort
};
}Updating nested objects
Mutating nested properties doesn't change the top-level object reference, so React won't detect the update at any level.
// Problem: Mutating a nested object
function UserProfile() {
const [user, setUser] = useState({
name: "Alice",
settings: { theme: "light", notifications: true },
});
const toggleTheme = () => {
user.settings.theme = "dark"; // Mutating a nested property
setUser(user); // Same object reference
};
}// Recommended: Create new objects at every level
function UserProfile() {
const [user, setUser] = useState({
name: "Alice",
settings: { theme: "light", notifications: true },
});
const toggleTheme = () => {
setUser({
...user,
settings: {
...user.settings,
theme: "dark",
},
});
};
}Mutating props directly
Props are owned by the parent component. A child should never modify its props.
// Problem: Mutating props directly
function Component(props) {
props.name = "Bob"; // Child components should not mutate props
// ^^^ Do not mutate 'props' directly. Props and state are immutable — create a new object instead.
return <div>{props.name}</div>;
}// Recommended: Notify the parent via a callback
function Component({ name, onNameChange }) {
return <button onClick={() => onNameChange("Bob")}>{name}</button>;
}Complete component examples
Real components often mix state updates with JSX. Here are fuller examples showing how mutations silently break rendering in complete component contexts.
// Problem: Mutating the array in a list component
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (id, text) => {
todos.push({ id, text }); // Directly mutating the original array
setTodos(todos); // Same reference, won't trigger a re-render
};
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}// Recommended: Create a new array so React detects the change
function TodoList() {
const [todos, setTodos] = useState([]);
const addTodo = (id, text) => {
setTodos([...todos, { id, text }]); // New array
};
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}// Problem: sort mutates the state array inside a button handler
function SortedList() {
const [items, setItems] = useState([3, 1, 2]);
const sort = () => {
setItems(items.sort()); // items is mutated!
};
return <button onClick={sort}>Sort</button>;
}// Recommended: Spread into a new array before sorting
function SortedList() {
const [items, setItems] = useState([3, 1, 2]);
const sort = () => {
setItems([...items].sort());
};
return <button onClick={sort}>Sort</button>;
}Options
Shared Settings
Custom state hooks can also be configured via shared ESLint settings, which apply consistently across all rules in the plugin:
{
"settings": {
"react-x": {
"additionalStateHooks": "/^(useMyState|useCustomState)$/u"
}
}
}Versions
Resources
Further Reading
- React Docs: Updating Objects in State
- React Docs: Updating Arrays in State
- React Docs: Components and Hooks must be pure
- React Docs:
immutabilityLint Rule
See Also
react-x/no-direct-mutation-state
Disallows direct mutation ofthis.state.react-x/purity
Validates that components and hooks are pure by checking that they do not call known-impure functions during render.react-x/use-state
Enforces correct usage ofuseState, including destructuring, symmetric naming of the value and setter, and wrapping expensive initializers in a lazy initializer function.