When useState Is Asynchronous & Some Ways To Coerce React Not To Wait
jbrochu1
Posted on November 4, 2022
Have you ever had the situation with a React application where your Components browser tool was showing you the useState/setState data you expect immediately upon your event but the application doesn’t render the data until an additional click is performed? That is exactly the situation I personally found myself in last week. The application was a matching game and some desired features were an attempt counter along with a matched counter. These 2 particular features were to be updated immediately with an onClick event.
Here is a more detailed explanation of what I experienced. See the browser inspect screenshot and initial code as well for more context. Hook #3 (totalAttempts) would update immediately as expected upon the event trigger (this was a simple counter to keep track of the number of attempts). #4 (matchCount) was also a simple counter that was supposed to be triggered immediately if 2 different useStates matched (matching the image name with the displayed text name). However, what actually happened was the useState would not register the match until an extra click occurred (so 3 clicks total to match 2 items). The React rendered onscreen the same as described above with an extra click needed to register the match.
export default function GamePage({animals, onAdd}) {
const [chosenName, setChosenName] = useState("1");
const [chosenImg, setChosenImg] = useState("2");
const [chosenCount, setChosenCount] = useState(0);
const [matchCount, setMatchCount] = useState(0);
const [totalAttempts, setTotalAttempts] = useState(0);
const [isVisible, setIsVisible] = useState(false);
const HandleChosenName = (name) => {
setChosenCount(chosenCount + 1)
setChosenName(name)
setTotalAttempts(totalAttempts + 0.5)
if (chosenImg === chosenName)
{
setMatchCount(matchCount + 1);
resetTurn();
}
if (chosenCount > 1) {
resetTurn();
}
else if (matchCount > 3) {
console.log("game over");
setIsVisible(!isVisible);
}
}
const HandleChosenImg = (name) =>{
setChosenCount(chosenCount + 1)
setChosenImg(name)
setTotalAttempts(totalAttempts + 0.5)
if (chosenImg === chosenName)
{
setMatchCount(matchCount + 1);
resetTurn();
}
if (chosenCount > 1) {
resetTurn();
}
else if (matchCount > 3) {
console.log("game over");
setIsVisible(!isVisible);
}
}
One of the neat things about React is that any changes to the props (data that is passed around between components) or the useStates (think of these as special React variables), React will re-render the affected components when it detects the changes without additional code.
Unfortunately there are occasionally some not so neat side effects in the way React processes these detected changes and re-renders. My understanding of React under the hood is that the rendering occurs in batches determined by React’s algorithms. I also understood that some renders could be taxing and require quite a bit of resources. Depending on the complexity of the application and the quantity of renders in the queue, some of the renders may delayed until the next batch. This is intended to make React run the application more efficiently. When the render is delayed this what causes the operation to be defined as asynchronous (it is not actually rendering when the setState sets the useState variable until later).
Taking a dive you may also see a handful of other issues that can contribute to this behavior. These will be spared from the scope of this post to keep it more concise but if you want to go deeper look at some keywords of flushing and reconciling in regards to how React works under the hood.
If you do run into a similar situation a potential workaround is to utilize the useEffect hook. This will require React to do a more immediate render. The useEffect hook, for those of you not familiar, will keep an eye on particular states if you specify them in order to do just that. Here is what the code looks like with the use effect implemented.
const HandleChosenName = (name) => {
setChosenCount(chosenCount + 1)
setChosenName(name)
setTotalAttempts(totalAttempts + 0.5)
}
const HandleChosenImg = (name) =>{
setChosenCount(chosenCount + 1)
setChosenImg(name)
setTotalAttempts(totalAttempts + 0.5)
}
useEffect(() => {
if (chosenImg === chosenName)
{
setMatchCount(matchCount + 1);
resetTurn();
}
if (chosenCount > 1) {
resetTurn();
}
else if (matchCount > 3) {
console.log("game over");
setIsVisible(!isVisible);
}
}, [chosenImg, chosenName])
Now with this simple addition of useEffect, the matchCount useState would register immediately upon the the second click. Problem solved! Hopefully this will come in handy for others experiencing a similar issue!
Posted on November 4, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024