开发者问题收集

递归中状态未更新

2021-06-30
529
export default function App() {
  const [actionId, setActionId] = useState("");

  const startTest = async () => {
    const newActionId = actionId + 1;
    setActionId(newActionId);

    const request = {
      actionId: newActionId
    }

    console.log({ request });
    // const response = await api.runTests(request)

    await new Promise((resolve) => setTimeout(resolve, 4000));
    startTest();
  };

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <button onClick={startTest}>Start</button>
    </div>
  );
}

request actionId 始终为 1,尽管我每 4 秒更改一次。

我知道 setState 是异步的,但奇怪的是 4 秒后状态没有更新。

1个回答

理论

渲染的 React 组件包含属于 渲染的函数。如果这些函数引用了过时的变量,则会影响结果。搜索 过时的闭包 以了解有关此内容的更多信息。在 React 中处理传统 Javascript 函数 setTimeoutsetInterval 时,特别容易出现过时的闭包。

那么发生了什么?

因此,在第一次渲染时,存在一个特定的 startTest 实例。单击按钮时,将运行该 startTest 实例。 startTest 的这个实例包含一个 actionId 闭包(顺便问一下,为什么要将 actionId 设置为 "" ,然后对其执行加法?更期望从 0 开始,或者通过 + "1" 执行加法)。设置状态后,React 会重新渲染,将 actionId 设置为 "1" ,并使用新版本的 startTest 重新渲染,其中包含一个闭包,其中 actionId"1" 。但是,只有再次单击按钮时才会触发此函数。相反,第一次渲染的超时会触发对第一次渲染的 startTest 的新调用。此超时不知道组件已重新渲染,并且其他地方有新版本的 startTest ,其中更新了 actionId 闭包。当重新触发该函数时,它会计算与第一次相同的 newActionId 值,因此我们陷入了一个循环,该循环仅使用包含过时闭包的 startTest 的第一个实例。

如何解决?

如果您想在 React 中使用超时和间隔,您必须按照 React 的方式进行。可能有适合此目的的软件包,但如果您愿意,可以稍微更改示例以使其正常工作。

您可以向 setActionId 提供一个函数,该函数将前一个值作为输入,而不是计算 newActionId 的值:

setActionId(oldActionId => oldActionId + 1)

现在,由于前一个值始终作为输入传递,因此过时的闭包不会影响函数的结果,您的示例将正常工作。但是,我不确定设计是否正确,因为如果用户再次按下按钮,您将同时运行多个超时,从长远来看会造成严重破坏。尽管如此,如果您想确保它按预期工作,您可以尝试这种方式。如果您可以通过限制按钮只能按一次来保证该函数只执行一次,那么这将是一个更好的设计。

祝你好运!

fast-reflexes
2021-06-30