【问题标题】:Why is useState within useEffect not working in React?为什么 useEffect 中的 useState 在 React 中不起作用?
【发布时间】:2022-01-06 05:42:07
【问题描述】:

我使用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}}

【问题讨论】:

    标签: javascript reactjs use-effect use-state


    【解决方案1】:

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

    当您调用setCounter 时,它会更新商店中的counter,并且钩子会重新渲染。效果挂钩不会再次运行,因为没有任何依赖项([] - 没有)已更改。

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

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


    函数传递给setCounter,以获取最新的值而不是原来的结束值:

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

    【讨论】:

      【解决方案2】:

      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

      【讨论】:

      • 这是有效的,但是当 VSC 在保存后自动重新加载浏览器时,计数器将值加倍,并且我的计数比数据库中的记录多。我需要在 useEffect 开始时将计数器设置为零吗?
      • @JohnDoener 我已经为这个问题添加了一个可能的解决方案。是的,您可以在循环外添加setCounter({mona:0, phil:o}),但是,恕我直言,如果您要重新计算整个状态,最好不使用反应钩子就这样做,并在最后调用setCounter 一次并重新计算最终状态
      猜你喜欢
      • 2023-01-17
      • 2022-07-06
      • 2020-07-15
      • 2021-09-06
      • 2021-12-04
      • 2020-10-18
      • 1970-01-01
      • 1970-01-01
      • 2022-07-20
      相关资源
      最近更新 更多