React version: 17.0.1
Steps To Reproduce
- Enable strict mode for checking for possible issues in the future concurrent mode
- create the below component and run the code
import { useCallback, useState } from "react";
const Example = ({ onIncrement }) => {
const [count, setCount] = useState(0);
const incrementHandler = useCallback(() => {
onIncrement(count, count + 1); // Is count guaranteed to be the latest state here due to including count in the useCallback dependency array?
setCount((count) => count + 1);
}, [count, onIncrement]);
return (
<>
<span>{count}</span>
<button onClick={incrementHandler}>increment</button>
</>
);
};
const Parent = () => (
<Example
onIncrement={(currentCount, incrementedCount) =>
alert(
`count before incrementing: ${currentCount}, after increment: ${incrementedCount}`
)
}
/>
);
export default Parent;
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:
const incrementHandler = useCallback(() => {
setCount((count) => {
onIncrement(count, count + 1);
return count + 1
});
}, [count, onIncrement]);
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 in useEffect callback with count and onIncrement in effect's dependencies array how can I know that the onClick 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:
const incrementHandler = useCallback(() => {
let prevCount, nextCount;
setCount(
(count) => {
prevCount = count;
nextCount = count + 1;
return nextCount;
},
() => onIncrement(prevCount, nextCount)
);
}, [onIncrement]);
In my humble opinion, this doesn't collide with the async nature of setCount and can be implemented.
unlike belowgetState 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 because setState is not executed yet.
wrong solution:
const [count, setCount, getCount] = useState(0);
const incrementHandler = useCallback(() => {
setCount((count) => count + 1);
const currentCount = getCount();
const nextCount = currentCount + 1;
onIncrement(currentCount, nextCount)
}, [onIncrement]);
or providing a third array to useCallback for accessing the latest state can not be implemented due to the same problem with getState 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!
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
incrementHandlerfunction like below:then the
onIncrementwill 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
onIncrementinuseEffectcallback withcountandonIncrementin effect's dependencies array how can I know that theonClickevent 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
setStatelike 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
setCountand can be implemented.unlike below
getStateproposals 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 becausesetStateis not executed yet.wrong solution:
or providing a third array to
useCallbackfor accessing the latest state can not be implemented due to the same problem withgetStateand 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!