Bug: useState apply updaters order does not comply with the documentation
React version: 16 & 18
Steps To Reproduce
- Call setState(functional updater) twice
- One of the updater will be calculated before re-render.
Link to code example: https://codesandbox.io/p/sandbox/quizzical-hugle-wsp7zh
The expected behavior
As per the documentation of setState,
If you pass a function as nextState, it will be treated as an updater function. It must be pure, should take the pending state as its only argument, and should return the next state. React will put your updater function in a queue and re-render your component. During the next render, React will calculate the next state by applying all of the queued updaters to the previous state.
Expected behaviour is,
- click on the button
- enqueue two updater actions
- the App component re-renders
- (when visiting useState) apply two actions
- get the updated new value
The current behavior
However the current behavior is,
- click on the button
- one of the actions is calculated IMMEDIATELY, the rest are enqueued.
- the App component re-renders
- (when visiting useState) apply the remaining actions
- get the updated new value
Note
Only the first time not complying. From the second time it works expectedly.
@gwy15 : yeah, there's a lot more nuance to it than what is described in the docs. In some cases React may try to check if the update would result in an identical value and bail out even before trying to re-render. The logic is... complicated :)
I wrote up a related explanation here:
- https://github.com/facebook/react/issues/28725#issuecomment-2033506985
Ultimately the docs often gloss over the specific implementation details of rendering. Most of the time that's fine, and it's not a thing you need to worry about the nuances of.
I agree that most of the time this eager evaluation is fine, since the updater SHOULD be a pure function, and thus it should not matter when it's called or in what order. I only came across this unexpected behavior because in my case I couldn't think of a better way but to create an updater with side effect, which is affected by when the updater is called.
Another thing I want to mention here for potential future reference, useReducer() works exactly the way it is supposed to, as in "call dispatch - enqueue - rerender component - call useReducer - call & reduce action". No eager optimization.
I would appreciate if the documention could state somthing like
If the updater function meets those requirements (being pure), React will act as if it put your updater function in a queue and re-render your component. During the next render, React will calculate the next state by applying all of the queued updaters to the previous state.
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!