53

I'm reading React Hook documentation about functional updates and see this quote:

The ”+” and ”-” buttons use the functional form, because the updated value is based on the previous value

But I can't see for what purposes functional updates are required and what's the difference between them and directly using old state in computing new state.

Why functional update form is needed at all for updater functions of React useState Hook? What are examples where we can clearly see a difference (so using direct update will lead to bugs)?

For example, if I change this example from documentation

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    </>
  );
}

to updating count directly:

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(count + 1)}>+</button>
      <button onClick={() => setCount(count - 1)}>-</button>
    </>
  );
}

I can't see any difference in behaviour and can't imagine case when count will not be updated (or will not be the most recent). Because whenever count is changing, new closure for onClick will be called, capturing the most recent count.

likern
  • 3,744
  • 5
  • 36
  • 47

5 Answers5

41

State update is asynchronous in React. So it is possible that there would be old value in count when you're updating it next time. Compare, for example, result of these two code samples:

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => {
        setCount(prevCount => prevCount + 1); 
        setCount(prevCount => prevCount + 1)}
      }>+</button>
    </>
  );
}

and

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => {
        setCount(count + 1); 
        setCount(count + 1)}
      }>+</button>
    </>
  );
}
Alex Gessen
  • 550
  • 5
  • 4
  • 1
    This is a different case. update function is considered async meaning you shouldn't expect immediate value change for count after setClount call and, thus can't rely on that new value. I get it, but that is a different use case than mine. What's problem with replacing state once with new, which is based on old (as most people do) – likern Sep 06 '19 at 21:26
  • 8
    This is exactly the reason - user can click on + and - before the rerender of the component or click on the + twice very quick and in the second call the value of `count` would be wrong (not updated). – Alex Gessen Sep 06 '19 at 22:10
  • 1
    Yes, I think I have to agree with that possible scenario – likern Sep 07 '19 at 00:19
  • 1
    @AlexGessen, assume I click on + rapidly twice. Initial value is 0. The count should increment to 2 i.e, 0 -> 1 and then 1 -> 2. Without the functional updater form, there is a chance that when the second setCount(count + 1) is being executed, the count has not been updated to 1 yet, so it sets count to 0 + 1, using the old value of count - 0. In a similar situation but with the functional updater, does that mean that when the 2nd setCount takes place (setCount(prevCount => prevCount + 1)), prevCount here will be updated even if count itself still might not be updated? – bhagwans Oct 09 '20 at 19:51
  • 1
    What really confuses me is that their hooks hello world at: https://reactjs.org/docs/hooks-state.html just does `onClick={() => setCount(count + 1)}`. Giving a wrong hello world is evil! – Ciro Santilli OurBigBook.com Jan 01 '22 at 11:24
  • 3
    Clicking fast will _not_ mess up your state, and the Hooks hello world is _not_ wrong. See my answer: https://stackoverflow.com/a/73115899/388033 – Vanessa Phipps Jul 25 '22 at 22:21
29

I stumbled into a need for this recently. For example let's say you have a component that fills up an array with some amount of elements and is able to append to that array depending on some user action (like in my case, I was loading a feed 10 items at a time as the user kept scrolling down the screen. the code looked kind of like this:

function Stream() {
  const [feedItems, setFeedItems] = useState([]);
  const { fetching, error, data, run } = useQuery(SOME_QUERY, vars);

  useEffect(() => {
    if (data) {
      setFeedItems([...feedItems, ...data.items]);
    }
  }, [data]);     // <---- this breaks the rules of hooks, missing feedItems

...
<button onClick={()=>run()}>get more</button>
...

Obviously, you can't just add feedItems to the dependency list in the useEffect hook because you're invoking setFeedItems in it, so you'd get in a loop.

functional update to the rescue:

useEffect(() => {
    if (data) {
      setFeedItems(prevItems => [...prevItems, ...data.items]);
    }
  }, [data]);     //  <--- all good now
G Gallegos
  • 609
  • 6
  • 11
16

The “state update is asynchronous in React” answer is misleading, as are some comments below it. My thinking was also wrong until I dug into this further. You are right, this is rarely needed.

The key idea behind functional state updates is that state you depend on for the new state might be stale. How does state get stale? Let’s dispel some myths about it:

  • Myth: State can be changed under you during event handling.
    • Fact: The ECMAScript event loop only runs one thing at a time. If you are running a handler, nothing else is running alongside it.
  • Myth: Clicking twice fast (or any other user action happening quickly) can cause state updates from both handler calls to be batched.
    • Fact: React is guaranteed to not batch updates across more than one user-initiated event. This is true even in React 18, which does more batching than previous versions. You can rely on having a render in between event handlers.

From the React Working Group:

Note: React only batches updates when it’s generally safe to do. For example, React ensures that for each user-initiated event like a click or a keypress, the DOM is fully updated before the next event. This ensures, for example, that a form that disables on submit can’t be submitted twice.

So when do you get stale state?

Here are the main 3 cases I can think of:

Multiple state updates in the same handler

This is the case already mentioned where you set the same state multiple times in the same handler, and depend on the previous state. As you pointed out, this case is pretty contrived, because this clearly looks wrong:

  <button
    onClick={() => {
      setCount(count + 1);
      setCount(count + 1);
    }}
  >+</button>

A more plausible case is calling multiple functions that each do updates on the same state and depend on the previous state. But that’s still weird, it’d make more sense to do all the calculations then set the state once.

Async state updates in a handler

For example:

  <button
    onClick={() => {
      doSomeApiCall().then(() => setCount(count + 1));
    }}
  >+</button>

This is not so obviously wrong. The state can be changed in between you calling doSomeApiCall and when it resolves. In this case, the state update really is async, but you made it that way, not React!

The functional form fixes this:

  <button
    onClick={() => {
      doSomeApiCall().then(() => setCount((currCount) => currCount + 1));
    }}
  >+</button>

Updating state in useEffect

G Gallegos's answer pointed this out for useEffect in general, and letvar's answer pointed this out for useEffect with requestAnimationFrame. If you're updating state based on previous state in useEffect, putting that state in the dependency array (or not using a dependency array) is a recipe for infinite loops. Use the functional form instead.

Summary

You don’t need the functional form for state updates based on previous state, as long as you do it 1. in a user-triggered-event handler 2. once per handler per state and 3. synchronously. If you break any of those conditions, you need functional updates.

Some people might prefer to always use functional updates, so you don’t have to worry about those conditions. Others might prefer the shorter form for clarity when it’s safe to do so, which is true for many handlers. At that point it’s personal preference / code style.

Historical note

I learned React before Hooks, when only class components had state. In class components, “multiple state updates in the same handler” doesn’t look so obviously wrong:

  <button
    onClick={() => {
      this.setState({ count: this.state.count + 1 });
      this.setState({ count: this.state.count + 1 });
    }}
  >+</button>

Since state is an instance variable instead of a function parameter, this looks fine, unless you know that setState batches calls when in the same handler.

In fact, in React <= 17, this would work fine:

  setTimeout(() => {
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
  }, 1000);

Since it’s not an event handler, React re-renders after each setState call.

React 18 introduces batching for this and similar cases. This is a useful performance improvement. There is the downside that it breaks class components that rely on the above behavior.

References

Vanessa Phipps
  • 2,205
  • 1
  • 18
  • 22
  • I still don't quite get it, for `` , the first click make a state change, since react state update is async, let's say it is held for 100 millisecond before it is "commited", if my second click is quick enough and make a second state change, and this change still refer to the stale `counter` (because it is hasn't been updated) – lch Aug 02 '23 at 07:33
10

I have answered a similar question like this and it was closed because this was the canonical question - that i did not know of, upon looking the answers i decided to repost my answer here since i think it adds some value.

If your update depends on a previous value found in the state, then you should use the functional form. If you don't use the functional form in this case then your code will break sometime.

Why does it break and when

React functional components are just closures, the state value that you have in the closure might be outdated - what does this mean is that the value inside the closure does not match the value that is in React state for that component, this could happen in the following cases:

1- async operations (In this example click slow add, and then click multiple times on the add button, you will later see that the state was reseted to what was inside the closure when the slow add button was clicked)

const App = () => {
  const [counter, setCounter] = useState(0);

  return (
    <>
      <p>counter {counter} </p>
      <button
        onClick={() => {
          setCounter(counter + 1);
        }}
      >
        immediately add
      </button>
      <button
        onClick={() => {
          setTimeout(() => setCounter(counter + 1), 1000);
        }}
      >
        Add
      </button>
    </>
  );
};

2- When you call the update function multiple times in the same closure

const App = () => {
  const [counter, setCounter] = useState(0);

  return (
    <>
      <p>counter {counter} </p>
      <button
        onClick={() => {
          setCounter(counter + 1);
          setCounter(counter + 1);
        }}
      >
        Add twice
      </button>
   
    </>
  );
}
ehab
  • 7,162
  • 1
  • 25
  • 30
1

Another use case for using functional updates with setState - requestAnimationFrame with react hooks. Detailed information is available here - https://css-tricks.com/using-requestanimationframe-with-react-hooks/

In summary, handler for requestAnimationFrame gets called frequently resulting in incorrect count value, when you do setCount(count+delta). On the other hand, using setCount(prevCount => prevCount + delta) yields correct value.

letvar
  • 11
  • 1