bun/react-prefer-updater-function

Prefer setItems(previous => ..) to a new array or object made from the state of the last render

The items in const [items, setItems] = useState([]) is the state as of the last render. If setItems([...items, item]) runs after an await, in a timer, or twice before the next render, items is out of date and the other update is lost. A function gets the current state: setItems(previous => [...previous, item]).

The rule reports a setter that's called with an array or object literal that spreads its own state.

Configure#

Off by default. To turn it on:

bunfig.toml
[lint.rules]
"bun/react-prefer-updater-function" = "error" # or "warn"

Examples#

example.jsx1 problem
import { useState } from "react";

function Cart() {
  const [items, setItems] = useState([]);

  async function add(id) {
    const item = await loadItem(id);
    setItems([...items, item]);
  }
  // ...
}
example.jsx1 problem
import { useState } from "react";

function Profile() {
  const [form, setForm] = useState({ name: "", email: "" });

  return <input value={form.name} onChange={event => setForm({ ...form, name: event.target.value })} />;
}
example.jsxNo problems
import { useState } from "react";

function Cart() {
  const [items, setItems] = useState([]);

  async function add(id) {
    const item = await loadItem(id);
    setItems(previous => [...previous, item]);
  }
  // ...
}

If neither name comes from a useState() in the file, for example because both are props, the rule goes by the names. setRows is treated as the setter for rows:

example.jsx1 problem
function AddRow({ rows, setRows }) {
  return <button onClick={() => setRows([...rows, { id: rows.length }])}>Add</button>;
}

Spreading something else is fine:

example.jsxNo problems
import { useState } from "react";

function Search({ defaults }) {
  const [filters, setFilters] = useState({});

  return <button onClick={() => setFilters({ ...defaults })}>Reset</button>;
}