-
Notifications
You must be signed in to change notification settings - Fork 47.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Question about getting the latest state value in the concurrent mode #20924
Comments
I skimmed this, but I think this is key of the problem you're reporting: const incrementHandler = () =>
setCount((count) => {
// Side effects aren't safe here!
// State updater functions should be pure!
onIncrement(count, count + 1);
return count + 1;
}); (I've removed The Fortunately the fix for this is tiny: const incrementHandler = () => {
const newCount = count + 1;
// Event handlers can have side effects!
// Calling onIncrement here even has an added benefit:
// If onIncrement also updates state, the updates will get batched by React — which is faster!
onIncrement(count, newCount);
// You can also use the simpler updater form of just passing the new value in this case.
setCount(newCount);
}; If you want or need to memoize const incrementHandler = useCallback(() => {
// Because count is part of the dependencies,
// you have the latest value here (just like the example above).
const newCount = count + 1;
onIncrement(newCount);
setCount(newCount);
}, [count, onIncrement]); (Going to leave this issue open for a bit in case my answer above reflects a misunderstanding of the question you're asking.) |
Thank you so much for answering my question! The source of my confusion was articles that said react may postpone state updates due to heavy workload and that you shouldn't rely on the state value outside of setState callback. If I've understood well, React will invoke the render phase after each state change batch, like event handlers in my case, and for sure it will invoke render phases in order but may choose to postpone them or not to commit them in concurrent mode. Am I right? The other source of my confusion was the below quote from 'React docs':
which I had misunderstood and I thought that React might choose to ignore invoking some render phase completely, but the reality is that react will invoke it but might choose not to commit it. |
@hosseini44444 Can you describe what your |
In the scenario you posted above though, it seemed like you wanted to trigger some side effect ( |
This is the code which I'm using in a slider component:
The component has a lot of event handlers and one of them is
|
Libraries I've worked on (like |
Consider a scenario in which the user has clicked a thumb that will change the currentIndex to the thumb's index but due to heavy workload react has decided to postpone the render and in the meanwhile, the user clicks the next button, the action of clicking the thumb will invoke the render phase then React may decide to postpone it or decide not to commit it due to another user interaction like clicking the next button or even typing the index in an input directly or reading it from the backend. In this scenario, if react decides to invoke but not commit the first render phase which is an effect of the user clicking a thumb then the user will see the slide that is shown before clicking the thumb but after the first render phase the state changes to the index of the thumb which was clicked but will not see the result and then the second render phase will begin and will be committed then what the user will see on screen is 1st slide switching to 3rd slide but what he will get as So What You See Is Not What You Get (WYSINWYG). It may confuse or seem like a bug to the end-user in some scenarios, don't you agree? |
I think regardless of when React performs the update (soon vs deferring it) when the action was performed, ie when the user clicked, the count/index that was shown on the screen was the one that was closed over by the event handler that's getting called in your example code above. So it seems best to call the As Dan said above though, it can be difficult talking about this stuff abstractly. |
I think one crucial nuance we're overlooking here is React will not actually do any of that. In particular, React does offer a guarantee that you will not get a render of the "next click" before the "previous click" has been flushed. This has already been a guarantee with Concurrent Mode, but we're making it even a stronger one — click events (and similar "intentional" user event like keyboard input) will always flush without interruption in a microtask. In practice, this means events for clicks and other intentional events will never get "ignored" as in this hypothetical scenario we're discussing. We'll include this in the documentation when it's stable. |
Thanks so much. |
React version: 17.0.1
Steps To Reproduce
The current behavior
In this simple example everything seems to be fine but in a more complicated situation full of event handlers that change the count or async callbacks that may change the count( like data fetching callbacks) the count value is not guaranteed to be the latest state and if I change the
incrementHandler
function like below:then the
onIncrement
will run twice in development while in strict mode and may run twice in production in concurrent mode according to documentation.and If you suggest running the
onIncrement
inuseEffect
callback withcount
andonIncrement
in effect's dependencies array how can I know that theonClick
event of the increment button has caused the effect and not another event for example decrement or anything else.you may say by setting another state that shows what is responsible for the effect, then I may need the previous state which unlike this example may be impossible to recalculate.
you may suggest using a ref for storing the previous state (count) then I will end up with one extra state or ref for storing what is responsible for the effect to run, one extra ref for storing the previous state, and a useEffect hook to run the onIncrement click event handler
The expected behavior
Providing a second callback argument to
setState
like in class Components that will run after this state update so we can save the current and next state and use it in the callback like below:In my humble opinion, this doesn't collide with the async nature of
setCount
and can be implemented.unlike below
getState
proposals that if it will be asynchronous it may not return the desired state. and if it will be synchronous it will not return the latest state too becausesetState
is not executed yet.wrong solution:
or providing a third array to
useCallback
for accessing the latest state can not be implemented due to the same problem withgetState
and async nature of setState.Please tell me if I'm missing something or I've misunderstood things.
If not, please tell me if there is a simple solution for this scenario or similar ones, or tell me the best practices for running a callback or event handler with the latest state.
Thank you!
The text was updated successfully, but these errors were encountered: