securethebags
securethebags

Reputation: 55

why is useEffect rendering unexpected values?

I am trying to create a scoreboard for a quiz application. After answering a question the index is updated. Here is the code for the component.

export const ScoreBoard = ({ result, index }) => {
    const [score, setScore] = useState(0)
    const [total, setTotal] = useState(0)
    const [rightAns, setRight] = useState(0)

    useEffect(() => {
        if(result === true ) { 
            setRight(rightAns + 1)
            setTotal(total + 1)

        }
        if(result === false) {
            setTotal(total + 1)
        }
        setScore(right/total)
    }, [index]);

    return (
        <>
        <div>{score}</div>
        <div>{rightAns}</div>
        <div>{total}</div>
        </>
    )

    }

When it first renders the values are

score = NaN
rightAns = 0
total = 0

After clicking on one of the corrects answers the values update to

score = NaN
rightAns = 1 
total = 1

and then finally after one more answer (with a false value) it updates to

score = 1
rightAns = 1
total = 2

Score is no longer NaN but it is still displaying an incorrect value. After those three renders the application begins updating the score to a lagging value.

score = 0.5
rightAns = 2
total = 3

What is going on during the first 3 renders and how do I fix it?

Upvotes: 0

Views: 331

Answers (4)

Zachary Haber
Zachary Haber

Reputation: 11027

With how you have it set up currently, you'd need to make sure that you are updating result before index. Because it seems like the useEffect is creating a closure around a previous result and will mess up from that. Here's showing that it does work, you just need to make sure that result and index are updated at the right times. If you don't want to calculate the score every render (i.e. it's an expensive calculation) you can useMemo or useEffect as I have shown in the stackblitz.

https://stackblitz.com/edit/react-fughgt

Although there are many other ways to improve how you work with hooks. One is to make sure to pay attention to the eslint react-hooks/exhaustive-deps rule as it will forcefully show you all the little bugs that can end up happening due to how closures work.

In this instance, you can easily calculate score based on total and rightAns. And total is essentially just index + 1.

I'd also modify the use effect as it is right now to use setState as a callback to get rid of a lot of dependency issues in it:

useEffect(() => {
  if (result === true) {
    setRight(rightAns => rightAns + 1);
    setTotal(total => total + 1);
  }
  if (result === false) {
    setTotal(total => total + 1);
  }
}, [index]);
useEffect(()=>{
  setScore(rightAns / total ||0);
},[rightAns,total])

Upvotes: 0

HermitCrab
HermitCrab

Reputation: 3274

All of your set... functions are asynchronous and do not update the value immediately. So when you first render, you call setScore(right/total) with right=0 and total=0, so you get NaN as a result for score. All your other problems are related to the same problem of setScore using the wrong values.

One way to solve this problem is to remove score from state and add it to the return like this:

return (
    <>
    {total > 0 && <div>{right/total}</div>}
    <div>{rightAns}</div>
    <div>{total}</div>
    </>
)

You also can simplify your useEffect:

useEffect(() => {
    setTotal(total + 1);
    if(result === true ) setRight(rightAns + 1);
}, [index]);

Upvotes: 1

Adam Jeliński
Adam Jeliński

Reputation: 1788

You shouldn't be storing the score in state at all, because it can be calculated based on other states.

All the state change calls are asynchronous and the values of state don't change until a rerender occurs, which means you are still accessing the old values.

export const ScoreBoard = ({ result, index }) => {
    const [total, setTotal] = useState(0)
    const [rightAns, setRight] = useState(0)

    useEffect(() => {
        if(result === true ) { 
            setRight(rightAns + 1)
            setTotal(total + 1)

        }
        if(result === false) {
            setTotal(total + 1)
        }
    }, [index]);

    const score = right/total
    return (
        <>
        <div>{score}</div>
        <div>{rightAns}</div>
        <div>{total}</div>
        </>
    )
}

Simpler and following the React guidelines about the single "source of truth".

Upvotes: 3

Daniel Geffen
Daniel Geffen

Reputation: 1862

Your problem is that calling setState doesn't change the state immediately - it waits for code to finish and renders the component again with the new state. You rely on total changing when calculating score, so it doesn't work.

There are multiple approaches to solve this problem - in my opinion score shouldn't be state, but a value computed from total and rightAns when you need it.

Upvotes: 1

Related Questions