为什么 useEffect 中的 useState 在 React 中不起作用?

Why is useState within useEffect not working in React?

我使用 useEffect() 获取 Firestore 快照并并行我想计算一个值:

  const [counter, setCounter] = useState({ mona: 0, phil: 0 });
  useEffect(() => {
    onSnapshot(q, (snapshop) => {
      setTasks(
        snapshop.docs.map((doc) => {
          if (doc.data().wer === "Mona") {
            console.log("Mona + 1"); // This get's executed as expected (e.g. 3 times)
            setCounter({ ...counter, mona: counter.mona + 1 });
          }
          if (doc.data().wer === "Phil") {
            console.log("Phil + 1"); // This get's executed as expected (e.g. 6 times)
            setCounter({ ...counter, phil: counter.phil + 1 });
          }
          return {
            ...doc.data(),
            id: doc.id,
            timestamp: doc.data().timestamp?.toDate().getTime(),
          };
        })
      );
      setLoading(false);
    });
  }, []);

  useEffect(() => {
    console.log({ counter }); //this get's executed only 2 times.
  }, [counter]);

map() 中的 console.log() 正确执行时,为什么 setCounter 没有执行或更新计数器正确?

console.log({ counter }); 顺便说一句,除了:

{counter: {mona: 0, phil: 0}}
{counter: {mona: 0, phil: 1}}

您传递给 useEffect 的函数关闭 counter 变量。

当您调用 setCounter 时,它会更新商店中的 counter,并且挂钩会重新呈现。效果挂钩 不再 运行 因为 none 的依赖项([] - 有 none)已经改变。

下一次触发由onSnapshot设置的事件处理程序时,它使用与上次counter相同的值。这意味着 counter.phil 仍然是 0 在 effect hook 中。您将 1 添加到 0 再次 并调用 setCounter 但此值与之前的值相同。

由于这次counter没有改变,所以依赖于counter值的第二个效果挂钩不会被触发。


函数传递给setCounter以获取最新值而不是原始关闭值:

setCounter((latestCounter) => { ...latestCounter, phil: latestCounter.phil + 1 });

React 有时会批量更新状态。这意味着您对 setCounter 的所有调用只会触发一种效果。

此外,函数内部 counter 的值也在函数结束时更新,因此您正在丢失更新。

你应该做什么:

  • 首先传递一个回调给setCounter而不是使用counter的值。所以改变:

    setCounter({ mona: counter.mona, phil: counter.phil + 1 });
    

    至:

    setCounter(counter => ({ mona: counter.mona, phil: counter.phil + 1 }));
    
  • 要强制多次调用 useEffect,您必须使用 ReactDOM.flushSync:

    选择退出批量更新
    import { flushSync } from 'react-dom';
    
    // ...
    
    flushSync(() => setCounter(counter => ({ mona: counter.mona, phil: counter.phil + 1 })));
    

    通过这种方式,您的 useEffect 应该在每次更改计数器时被调用。显然,这比批处理更新效率低。


由于每次调用 onSnapshot 时您都需要重新加载整个数据集,而不是简单地修改当前值。

在这种情况下,您可以这样做:

const newCounter = { mona: 0, phil: 0};
snapshop.docs.map((doc) => {
          if (doc.data().wer === "Mona") {
            console.log("Mona + 1"); // This get's executed as expected (e.g. 3 times)
            newCounter.mona += 1;
          }
          if (doc.data().wer === "Phil") {
            console.log("Phil + 1"); // This get's executed as expected (e.g. 6 times)
            newCounter.phil += 1;
          }
          // ...
});
setCounter(newCounter);

因此您只需计算结果并在循环外调用 setCounter 一次,并使用最终计数。在这种情况下,您不需要读取旧状态,因为您是从头开始重新计算的。

可以 保留旧代码并在循环外添加一个 setCounter({mona: 0, phil: 0}) ,但我相信它的效率低于在反应钩子之外计算值并且只调用 setCounter 一次。