为什么 useEffect 呈现意外值?

why is useEffect rendering unexpected values?

我正在尝试为测验应用程序创建记分牌。回答问题后,索引会更新。这是组件的代码。

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>
        </>
    )

    }

当它首次呈现时,值为

score = NaN
rightAns = 0
total = 0

点击其中一个正确答案后,值更新为

score = NaN
rightAns = 1 
total = 1

然后最后在一个答案(带有错误值)后更新为

score = 1
rightAns = 1
total = 2

分数不再是 NaN,但它仍然显示不正确的值。在这三个渲染之后,应用程序开始将分数更新为滞后值。

score = 0.5
rightAns = 2
total = 3

前 3 个渲染过程中发生了什么,我该如何解决?

您的问题是调用 setState 不会立即更改状态 - 它会等待代码完成并使用新状态再次呈现组件。你在计算score时依赖total变化,所以它不起作用。

有多种方法可以解决这个问题 - 在我看来 score 不应该是状态,而是在需要时从 totalrightAns 计算出的值。

您所有的 set... 函数都是异步的,不会立即更新值。因此,当您第一次渲染时,您调用 setScore(right/total) 并设置 right=0 和 total=0,因此您会得到 NaN 作为分数的结果。您所有其他问题都与使用错误值的 setScore 相同问题有关。

解决此问题的一种方法是从状态中删除分数并将其添加到 return,如下所示:

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

您还可以简化您的useEffect:

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

您根本不应该将分数存储在状态中,因为它可以根据其他状态进行计算。

所有状态更改调用都是异步的,状态值在重新呈现发生之前不会更改,这意味着您仍在访问旧值。

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>
        </>
    )
}

更简单并遵循关于 single "source of truth".

的 React 指南

根据您当前的设置方式,您需要确保在索引之前更新结果。因为 useEffect 似乎正在围绕先前的结果创建一个闭包,并且会因此而变得混乱。这里显示它确实有效,您只需要确保在正确的时间更新结果和索引。 如果您不想在每次渲染时都计算分数(即这是一项昂贵的计算),您可以使用 Memo 或 useEffect,正如我在 stackblitz 中所展示的那样。

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

尽管还有许多其他方法可以改进您使用挂钩的方式。一是确保注意 eslint react-hooks/exhaustive-deps 规则,因为它会强制向您展示所有由于闭包的工作方式而最终可能发生的小错误。

在这种情况下,您可以很容易地根据total和rightAns计算分数。总数基本上只是 index + 1.

我还会修改使用效果,因为现在使用 setState 作为回调来摆脱其中的很多依赖问题:

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])