TrueWill
TrueWill

Reputation: 25523

When are functional updates required for computations involving previous state?

According to the documentation for the useState React Hook:

If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.

So given

const [count, setCount] = useState(initialCount);

you can write

setCount(prevCount => prevCount + 1);

I understand the reason for using the updater function form with setState, as multiple calls may be batched. However

During subsequent re-renders, the first value returned by useState will always be the most recent state after applying updates.

So I'm not clear why the above example couldn't be written as

setCount(count + 1);

(which is how it's presented in Using the State Hook).

Is there a case where you must use functional updates with the useState hook to get the correct result?

(Edit: Possibly related to https://github.com/facebook/react/issues/14259 )

Upvotes: 5

Views: 401

Answers (1)

Ryan Cogswell
Ryan Cogswell

Reputation: 81026

The main scenarios when the functional update syntax is still necessary are when you are in asynchronous code. Imagine that in useEffect you do some sort of API call and when it finishes you update some state that can also be changed in some other way. useEffect will have closed over the state value at the time the effect started which means that by the time the API call finishes, the state could be out-of-date.

The example below simulates this scenario by having a button click trigger two different async processes that finish at different times. One button does an immediate update of the count; one button triggers two async increments at different times without using the functional update syntax (Naive button); the last button triggers two async increments at different times using the functional update syntax (Robust button).

You can play with this in the CodeSandbox to see the effect.

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

function App() {
  const [count, setCount] = useState(1);
  const [triggerAsyncIndex, setTriggerAsyncIndex] = useState(1);
  const [triggerRobustAsyncIndex, setTriggerRobustAsyncIndex] = useState(1);
  useEffect(
    () => {
      if (triggerAsyncIndex > 1) {
        setTimeout(() => setCount(count + 1), 500);
      }
    },
    [triggerAsyncIndex]
  );
  useEffect(
    () => {
      if (triggerAsyncIndex > 1) {
        setTimeout(() => setCount(count + 1), 1000);
      }
    },
    [triggerAsyncIndex]
  );
  useEffect(
    () => {
      if (triggerRobustAsyncIndex > 1) {
        setTimeout(() => setCount(prev => prev + 1), 500);
      }
    },
    [triggerRobustAsyncIndex]
  );
  useEffect(
    () => {
      if (triggerRobustAsyncIndex > 1) {
        setTimeout(() => setCount(prev => prev + 1), 1000);
      }
    },
    [triggerRobustAsyncIndex]
  );
  return (
    <div className="App">
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <br />
      <button onClick={() => setTriggerAsyncIndex(triggerAsyncIndex + 1)}>
        Increment Count Twice Async Naive
      </button>
      <br />
      <button
        onClick={() => setTriggerRobustAsyncIndex(triggerRobustAsyncIndex + 1)}
      >
        Increment Count Twice Async Robust
      </button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Edit Async state updates

Another possible scenario where functional updates could be necessary would be if multiple effects are updating the state (even if synchronously). Once one effect updates the state, the other effect would be looking at out-of-date state. This scenario seems less likely to me (and would seem like a poor design choice in most cases) than async scenarios.

Upvotes: 5

Related Questions