【问题标题】:State update being overridden by another (previous) state update?状态更新被另一个(以前的)状态更新覆盖?
【发布时间】:2021-09-05 20:14:01
【问题描述】:

我知道这似乎是一个不寻常的例子,但我似乎仍然无法准确解释为什么我在单击div 后从未在控制台上看到valueB 打印?

请注意,由于我在 setTimeout 中调用了两个设置状态调用,它们不是批处理的

function App() {
  let [a, setA] = React.useState();
  let [b, setB] = React.useState();

  React.useEffect(() => {
    console.log('Entering useEffect', a, b);

    return () => {
      console.log('Entering cleanup', a, b);

      setA(null);
      setB(null);
    };
  }, [a, b]);

  console.log('Render', a, b);

  return (
    <div
      onClick={() => {
        setTimeout(() => {
          setA('valueA');
          setB('valueB');
        }, 100);
      }}
    >
      <h1>Test App</h1>
    </div>
  );
}

ReactDOM.render(
  <App/>,
  document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

【问题讨论】:

  • 在效果清理中调用状态 updaters 对我来说似乎是错误的。这仅用于测试目的吗?如果您删除这些,您将逐渐获得充满值的ab。因此,这很可能是 问题 的原因,那么问题是从清理函数更新状态是否可接受?我找不到任何支持或反对它的建议,是吗?
  • @Yoshi 是的,正如我提到的,我也知道这是因为setB(null) 而发生的,但我对更确切的步骤感兴趣,了解发生了什么以及为什么valueB从未出现在控制台日志中。 PS 不,我没有看到建议不要在清理功能中使用设置状态。
  • 不寻常的例子,但似乎因为a 的值首先发生变化,所以它会在@987654333 的值之前触发useEffect 两次(一次通过单击,再次通过useEffect 清理) @ 变化。到它在超时调用setB 时,它已经被更新的更新覆盖了两次。这个example 表明React 正在排队状态更新,以防止b 的值在使用超时 时过时(“hello”)。
  • @PeterB 官方文档有说吗?有链接吗?
  • @PeterB 那不一样,它并没有说不要在清理函数中调用 setState 。在未安装的组件中调用状态更新程序是另一回事。

标签: javascript reactjs react-hooks


【解决方案1】:

通常useEffect 及其清理在渲染后被异步调用(在另一个堆栈上)。但是,如果您在计时器中调用 setState 并且有 useEffect 回调,它们会被急切地调用,但所有累积的清理状态更改都会在 setState 调用此操作之后调用。

因此,在您的示例中,当调用 setTimeout 处理程序时:

  • 调用setA 时:a 状态已更改,并且清理中的两个状态更改添加到等待状态更改队列
  • 调用setB 时:首先将valueB 应用于b,然后从清理中应用null(这里有点批处理

这会尝试模仿在实际批处理状态更新时的行为,就像在单击处理程序中一样(首先从单击处理程序应用状态更新,然后从 useEffect 应用状态更新)。

当您使用更新函数时,您可以更清楚地看到正在发生的事情:

function App() {
  Promise.resolve().then(() => console.log("**** another stack ****"));
  console.log("before useStateA");
  let [a, setA] = React.useState();
    console.log("between useStates");
  let [b, setB] = React.useState();
    console.log("after useStateB");
  

  React.useEffect(() => {
    console.log('Entering useEffect', a, b);

    return () => {
      console.log('Entering cleanup', a, b);

      setA(() => (console.log("setting a to null from cleanup"), null));
      setB(() => (console.log("setting b to null from cleanup"), null));
    };
  }, [a, b]);

  console.log('Render', a, b);

  return (
    <div
      onClick={() => {
        setTimeout(() => {
          console.log("****timer start****");
          setA(() => (console.log("setting a to valueA from timer"), "valueA"));
          console.log("between timer setters");
          setB(() => (console.log("setting b to valueB from timer"), "valueB"));
          console.log("****timer end****");
        }, 100);
      }}
    >
      <h1>Test App</h1>
    </div>
  );
}

ReactDOM.render(
  <App/>,
  document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

【讨论】:

  • 确实,通过使用功能集状态,现在我们可以更好地看到setB 来自清理在setB 超时之后被触发,这可能解释了为什么会发生这种情况。所以这似乎不是我想的反应错误,对吧?虽然我想如果你不熟悉它有点奇怪。
  • 不,这不是错误。渲染后应该异步调用效果。如果因为有另一个同步渲染而无法实现,则尽可能晚地调用它们。
  • 我知道效果是异步调用的,只是在这种情况下 react 会以这种方式批量更新(在 setB 超时后从清理中调用 setB)这一事实并不明显。
猜你喜欢
  • 2021-11-09
  • 1970-01-01
  • 2021-06-02
  • 2019-06-15
  • 2023-01-28
  • 1970-01-01
  • 2016-12-01
  • 1970-01-01
  • 2019-12-19
相关资源
最近更新 更多