Search code examples
javascriptreactjsuse-effect

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?


Solution

  • 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".