Reputation: 25523
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
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);
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