public-rules › React techs/react

Use functional updates when new state depends on old state

MEDIUM1.0.0
When to apply Before planning, writing, changing, or reviewing React state updates that compute the next value from the current one, especially inside callbacks, timers, or asynchronous code.

When the next state is computed from the current state, pass an updater function to the state setter, such as setItems((current) => [...current, item]).

Implementation

  • Use the updater form for increments, appends, removals, and toggles.
  • Use it in callbacks that may run after later renders, such as timers, subscriptions, and code after an await.
  • Setting state to a value that does not depend on the previous state, such as setName(nextName) or setCount(0), does not need the updater form.
  • Keep updater functions pure; React may call them more than once in development.

Rationale

A callback sees the state value from the render that created it. If the state changes before the callback runs, or several updates are queued at once, an update computed from that captured value overwrites the newer state. An updater receives the latest pending state instead, and it lets a memoized callback omit that state from its dependencies.

Examples

Application: Updates in a callback

Incorrect (counterexample):

function TodoList() {
  const [items, setItems] = useState<Array<Item>>([]);

  const removeItem = useCallback((id: string) => {
    setItems(items.filter((item) => item.id !== id));
  }, []);
  // ...
}

The callback captured the first render's items, so each removal restores every item added since.

Correct:

function TodoList() {
  const [items, setItems] = useState<Array<Item>>([]);

  const removeItem = useCallback((id: string) => {
    setItems((current) => current.filter((item) => item.id !== id));
  }, []);
  // ...
}

Application: Updates after asynchronous work

Incorrect (counterexample):

async function handleUpload(file: File) {
  const uploaded = await upload(file);
  setFiles([...files, uploaded]);
}

If two uploads finish close together, the second update is computed from a list that does not include the first, and one upload disappears.

Correct:

async function handleUpload(file: File) {
  const uploaded = await upload(file);
  setFiles((current) => [...current, uploaded]);
}

Validation

Search for state setters whose argument reads the same state variable, such as setCount(count + 1) or setItems([...items, item]). Each one inside a callback, timer, subscription, or asynchronous function should use the updater form.

A direct update in a synchronous event handler that runs once per render is not a violation, although the updater form is still acceptable there.