logoESLint React
Rules

unstable-rules-of-state

Enforces the Rules of State.

This rule is currently in rc and only available in v3.0.0 rc releases.

This rule is experimental and may change in the future or be removed. It is not recommended for use in production code at this time.

Full Name in eslint-plugin-react-x@rc

react-x/unstable-rules-of-state

Full Name in @eslint-react/eslint-plugin@rc

@eslint-react/unstable-rules-of-state

Features

🧪

Presets

Rule Details

This rule covers one concern:

  1. Using the updater function form of a useState setter when the update references the corresponding state variable.

Common Violations

Referencing State Directly in a Setter Call

When a useState setter is called with an expression that directly references the corresponding state variable, it may cause stale state bugs — especially inside event handlers, timeouts, or closures that capture an outdated value. Using the callback form ensures the update is always based on the latest state value.

Invalid

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      {/*                           ^^^ Use the updater function form of 'setCount' to ensure the update is based on the latest state. */}
      {count}
    </button>
  );
}
import { useState } from "react";

function Toggle() {
  const [enabled, setEnabled] = useState(false);

  return (
    <button onClick={() => setEnabled(!enabled)}>
      {/*                             ^^^ Use the updater function form of 'setEnabled' to ensure the update is based on the latest state. */}
      {enabled ? "On" : "Off"}
    </button>
  );
}
import { useState } from "react";

function UserEditor() {
  const [user, setUser] = useState({ name: "John", age: 25 });

  // ❌ Spreading state directly — stale state risk.
  const updateAge = () => setUser({ ...user, age: 30 });

  return <button onClick={updateAge}>Update Age</button>;
}
import { useState } from "react";

function ItemList() {
  const [items, setItems] = useState(["a", "b"]);

  // ❌ Calling a method on state directly — stale state risk.
  const addItem = () => setItems([...items, "c"]);

  return <button onClick={addItem}>Add</button>;
}

Valid

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  // ✅ Callback form — always gets the latest state.
  return (
    <button onClick={() => setCount((prev) => prev + 1)}>
      {count}
    </button>
  );
}
import { useState } from "react";

function Component() {
  const [count, setCount] = useState(0);

  // ✅ Setting a constant value does not reference state.
  return <button onClick={() => setCount(0)}>Reset</button>;
}
import { useState } from "react";

function Component() {
  const [count, setCount] = useState(0);

  // ✅ Already using the callback form.
  return (
    <button onClick={() => setCount((prev) => prev + 1)}>
      {count}
    </button>
  );
}
import { useState } from "react";

function Component() {
  const [user, setUser] = useState({ name: "John" });
  const newUserFromApi = { name: "Jane" };

  // ✅ Setting a value that does not reference the state variable.
  setUser(newUserFromApi);

  return <div />;
}
import { useState } from "react";

function Component() {
  const [count, setCount] = useState(0);
  const [total, setTotal] = useState(100);

  // ✅ Referencing a *different* state variable is fine.
  setCount(total);

  return <div />;
}

Resources

Further Reading

On this page