35

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,306
  • 5
  • 33
  • 46

4 Answers4

32

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
  • 452
  • 4
  • 4
  • 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
  • 7
    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
  • 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 Путлер Капут 六四事 Jan 01 '22 at 11:24
16

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
  • 439
  • 3
  • 11
7

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
  • 5,548
  • 20
  • 28
0

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
  • 1