react-hooks/immutability

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

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#

Reported:

function Price({ product }) {
  product.price = product.price / 2;
  return <span>{product.price}</span>;
}
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>;
}
function Box() {
  const style = { color: "red" };
  const box = <div style={style} />;
  style.color = "blue";
  return box;
}
import { useEffect } from "react";

const last = { title: "" };

function useTitle(options) {
  delete options.icon;
  last.title = options.title;
  useEffect(() => {
    document.title = options.title;
  });
}
function Counter() {
  let count = 0;
  const onClick = () => {
    count = count + 1;
  };
  return <button onClick={onClick}>{count}</button>;
}

Not reported:

function Price({ product }) {
  const discounted = { ...product, price: product.price / 2 };
  discounted.label = "Sale";
  return <span>{discounted.label}: {discounted.price}</span>;
}
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:

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