react-hooks/immutability

Disallow modifying props, state, and other values that React expects to stay unchanged

Also called react_hooks/immutability, react/immutability

React compares values by identity to decide what to render again. If you change an object in place, it's still the same object, so React sees no change and the screen keeps showing the old data. Changing a prop also changes the parent's copy.

The rule reports assigning to a property, or using delete on one, when the property belongs to:

  • props and the arguments of a hook
  • a value returned by useState, useContext, and other hooks
  • a value that was already passed to a hook or used in JSX
  • a variable declared outside the component

It also reports a local variable that's reassigned after render, for example in an event handler.

Configure#

Off by default. To turn it on:

bunfig.toml
[lint.rules]
"react-hooks/immutability" = "error" # or "warn"

Examples#

example.jsx1 problem
function Price({ product }) {
  product.price = product.price / 2;
  return <span>{product.price}</span>;
}
example.jsx1 problem
import { useState } from "react";

function Profile() {
  const [user, setUser] = useState({ name: "" });
  const rename = name => {
    user.name = name;
    setUser(user);
  };
  return <button onClick={() => rename("Ada")}>{user.name}</button>;
}
example.jsx1 problem
function Box() {
  const style = { color: "red" };
  const box = <div style={style} />;
  style.color = "blue";
  return box;
}
example.jsx2 problems
import { useEffect } from "react";

const last = { title: "" };

function useTitle(options) {
  delete options.icon;
  last.title = options.title;
  useEffect(() => {
    document.title = options.title;
  });
}
example.jsx2 problems
function Counter() {
  let count = 0;
  const onClick = () => {
    count = count + 1;
  };
  return <button onClick={onClick}>{count}</button>;
}
example.jsxNo problems
function Price({ product }) {
  const discounted = { ...product, price: product.price / 2 };
  discounted.label = "Sale";
  return <span>{discounted.label}: {discounted.price}</span>;
}
example.jsxNo problems
import { useState } from "react";

function Profile() {
  const [user, setUser] = useState({ name: "" });
  const rename = name => {
    setUser({ ...user, name });
  };
  return <button onClick={() => rename("Ada")}>{user.name}</button>;
}

The rule doesn't know what a method call does. This modifies the parent's array but isn't reported:

example.jsxNo problems
function Tags({ tags }) {
  tags.push("new");
  return <span>{tags.length}</span>;
}