import { useState } from 'react';
function App() {
const [number, setNumber] = useState(0);
function clickHandler() {
setNumber((n) => {
console.log('First updater function');
return n + 1;
});
setNumber((n) => {
console.log('Second updater function');
return n + 1;
});
console.log('Other code in the click handler');
}
return (
<>
<h1>{number}</h1>
<button onClick={clickHandler}>Increment number</button>
</>
);
}
export default App;
According to this page, https://beta.reactjs.org/learn/queueing-a-series-of-state-updates,
The function passed to state setter is called an updater function. React queues the updater function to be processed after all the other code in the event handler has run. During the next render, React goes through the queue and gives you the final updated state.
From the above statement, I understand that, when I click the button, console.log should appear in the below order:
However, the logs are displayed in the below order:
The above order was observed when I click the button first time only. Clicking the button second time or more, again the order changes as expected:
So I was wondering why it was behaving differently when clicking the button first time and that behaviour also not matching as stated in the above React documentation page?
Thank you.
The useState hook in React follows a synchronous state update mechanism. When you call setNumber in your event handler, it's invoking the updater function immediately, instead of queuing it for later execution.
That's why you're seeing the console log from the first updater function immediately when you click the button. After the first setNumber is processed, the control returns to your event handler, and it logs 'Other code in the click handler', then it processes the second setNumber call, hence the console log from the second updater function.
The difference in behavior you're noticing between the first click and subsequent clicks is due to React's batched update mechanism. On the first click, since the component is not in a batched update phase, each setNumber call is processed immediately and separately. However, on subsequent clicks, React has entered a batched update phase, so all state updates within the event handler are batched together and applied at once at the end of the event, which aligns with your expected console log order.
let state; // Current state
let updates = []; // Queue of updates
let isBatching = false; // Check if batching is enabled
function useState(initialValue) {
// Initialize the state
if (state === undefined) {
state = initialValue;
}
function setState(update) {
if (isBatching) {
// If we're in a batching phase, queue the update
updates.push(update);
} else {
// If not, apply the update immediately
state = update(state);
render();
}
}
return [state, setState];
}
function flushUpdates() {
// Apply all updates
for (let update of updates) {
state = update(state);
}
// Clear the updates queue
updates = [];
render();
}
// A simple render function
function render() {
console.log(`Rendered with state: ${state}`);
}
function clickHandler(setNumber) {
isBatching = true;
// Queue two updates
setNumber((n) => {
console.log('First updater function');
return n + 1;
});
setNumber((n) => {
console.log('Second updater function');
return n + 1;
});
console.log('Other code in the click handler');
// Then flush the updates
flushUpdates();
isBatching = false;
}
// Usage:
let [number, setNumber] = useState(0);
clickHandler(setNumber); // It will log 'First updater function', 'Second updater function', 'Other code in the click handler', 'Rendered with state: 2'
As an end-user of React, you're not supposed to depend on these implementation details. Your component should not rely on the exact order of state updates within a single event handler because it could be subject to change in future versions of React.
Update 2025-02-02:
The React documentation has indeed changed. Note that they say "processing state updates" now, not "processing updater functions" as before.
React waits until all code in the event handlers has run before processing your state updates.
I.e. state changes are processed after other code, but if no state is changed - given that you should never cause side effects inside an updater function - then the updater function essentially does nothing, and it doesn't matter when it is called. (Note that a console.log() execution is technically a "side effect".)
The short answer is probably:
That sentence (currently: "React queues this function to be processed after all the other code in the event handler has run") is indeed probably not 100% true.
I suggest to understand it as "... React queues state-relevant updater functions to be processed after all the other code in the event handler has run...".
The React developers still always work on React and try to optimize things as far as possible, considering different edge cases, probably sometimes accepting tradeoffs, which makes it less straight-forward to describe what is going on.
I can imagine that that sentence was true at some point, but optimizations might have been implemented in the meantime, which make the sentence un-true for some cases.
This detail is probably rarely relevant, but I agree that if the documentation makes a statement like this, you should be allowed to rely on it.
So the documentation should maybe ...
I have to guess here. I found some hints that might explain what is going on, but I might be totally wrong. Also React might have changed since version 18.2.0 which I am currently using.
React needs the resulting state only in the next render cycle, not in the current one. So I understand that in some cases, when there are no other reasons to re-render, and the state has not changed, React decides to skip the next render cycle altogether, as a performance optimization. But to know that the state hasn't changed, it needs to call the updater function.
Note that if the first updater function doesn't change the state, then even the second updater function gets called before the other code in the event handler (again only when called the first time):
function clickHandler() {
setNumber((n) => {
console.log('First updater function');
return n + 0; // <--- state is not changed
});
setNumber((n) => {
console.log('Second updater function');
return n + 1;
});
console.log('Other code in the click handler');
}
Results in the console output:
First updater function
Second updater function
Other code in the click handler
In the React source code, in the dispatchSetState function,
there is a note that might be related:
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With