Chapter 2 of 4
Updates Are Asynchronous
Why the value does not change immediately, and what batching means for your code.
Calling the setter does not change the variable you are holding. It schedules an update. The current render keeps the value it started with.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
console.log(count); // still the old value
}
return <button onClick={handleClick}>{count}</button>;
}This is not a bug. count is a const belonging to this render. The next render gets a fresh count with the new value.
Batching
React batches every state update triggered in the same event into one re-render. Three setter calls do not cause three renders.
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
// count goes from 0 to 1, not to 3All three calls read the same count from the current render, so all three schedule 'set it to 1'.
The updater function
When the next value depends on the previous one, pass a function. React calls it with the latest pending value, so the calls stack correctly.
function handleClick() {
setCount((previous) => previous + 1);
setCount((previous) => previous + 1);
setCount((previous) => previous + 1);
}
// count goes from 0 to 3Lazy initial state
The argument to useState is evaluated on every render even though it is only used once. If computing it is expensive, pass a function instead and React calls it only on the first render.
// readFromStorage() runs on every single render
const [items, setItems] = useState(readFromStorage());
// readFromStorage() runs once
const [items, setItems] = useState(() => readFromStorage());