首页 > 解决方案 > 如何访问 setState 回调之外的最新状态?

问题描述

由于 React 的异步特性setState以及经过大量研究后新引入的react 并发模式,我不知道在以下场景或任何其他场景中如何访问保证的最新状态。

非常感谢 React 专家的回答,尤其是来自 React 团队的回答。

请记住,这是一个非常简单的示例,真正的问题可能发生在一个充满状态更新、事件处理程序、改变状态的异步代码的复杂项目中,......

import { useCallback, useState } from "react";

const Example = ({ onIncrement }) => {
  const [count, setCount] = useState(0);

  const increment = useCallback(() => {
    onIncrement(count, count + 1);  // Is count guaranteed to be the latest state here due to including count in the useCallback dependency array?
    setCount((count) => count + 1);
  }, [count, onIncrement]);

  return (
    <>
      <span>{count}</span>
      <button onClick={increment}>increment</button>
    </>
  );
};

const Parent = () => (
  <Example
    onIncrement={(currentCount, incrementedCount) =>
      console.log(
        `count before incrementing: ${currentCount}, after increment: ${incrementedCount}`
      )
    }
  />
);

export default Parent;

你可能会说我可以onIncrementsetCount回调中调用,但是由于新的反应并发模式,你可以看到将来反应更新onIncrement可能会被调用两次并输出两次结果,这不是预期的结果。

提交阶段通常非常快,但渲染可能很慢。出于这个原因,即将到来的并发模式(默认情况下尚未启用)将渲染工作分成几部分,暂停和恢复工作以避免阻塞浏览器。这意味着 React 可能在提交之前多次调用渲染阶段生命周期,或者它可能在根本不提交的情况下调用它们(因为错误或更高优先级的中断)。

您已经可以(React 17 严格模式)看到onIncrement在开发模式和 React 并发模式成为默认后的特性中将调用两次,因此在生产中可能会以这种方式调用两次。

useCallback依赖项数组中包含 count 是否可以保证 count 是最新的状态值?

onIncrement您可能会建议在挂钩中调用,useEffect但这样我将无法访问以前的状态,与此示例不同,它可能无法重新计算。(使用 ref 存储先前的状态不是解决方案)

使用的另一个问题useEffect是我不确定这个事件处理程序(onIncrement)是效果的原因还是另一个处理程序中的状态更改或useEffect回调导致了效果。(存储额外的状态或参考来检测原因是多余的)

谢谢!

标签: javascriptreactjsstatesetstateusecallback

解决方案


我感到困惑的原因是文章说,由于工作量大,反应可能会推迟状态更新,并且您不应该依赖 setState 回调之外的状态值。经过大量研究并根据这个答案,现在我知道反应可能会批量更改状态,但它始终保持顺序。

如果我理解得很好,React 将在每个状态更改批次之后调用渲染阶段,就像我的情况下的事件处理程序一样,并且它肯定会按顺序调用渲染阶段,但可以选择推迟它们或不以并发模式提交它们.

我感到困惑的另一个原因是以下来自“React docs”的引用:

提交阶段通常非常快,但渲染可能很慢。出于这个原因,即将到来的并发模式(默认情况下尚未启用)将渲染工作分成几部分,暂停和恢复工作以避免阻塞浏览器。这意味着 React 可能在提交之前多次调用渲染阶段生命周期,或者它可能在根本不提交的情况下调用它们(因为错误或更高优先级的中断)。

我误解了,我认为 React 可能会选择完全忽略调用某些渲染阶段,但现实是 react 会调用它但可能选择不提交它。

幸运的是,解决这个问题的方法很小:

const incrementHandler = () => {
  const newCount = count + 1;

  // Event handlers can have side effects!
  // Calling onIncrement here even has an added benefit:
  // If onIncrement also updates state, the updates will get batched by React — which is faster!
  onIncrement(count, newCount);

  // You can also use the simpler updater form of just passing the new value in this case.
  setCount(newCount);
};

上面的代码是Brian Vaughn回复我在 React 的 github repo 中提出问题的答案。https://github.com/facebook/react/issues/20924

Dan Abramov的另一个回答非常明确:

我认为我们在这里忽略的一个关键细微差别是 React 实际上不会做任何事情。特别是,React 确实提供了保证,在刷新“上一次点击”之前,您不会获得“下一次点击”的渲染。这已经是并发模式的保证,但我们正在使它变得更强大——点击事件(以及类似的“有意”用户事件,如键盘输入)将始终刷新而不会中断微任务。在实践中,这意味着点击事件和其他故意事件永远不会像我们正在讨论的假设场景中那样被“忽略”。当它稳定时,我们会将其包含在文档中。

谢谢丹和布赖恩


推荐阅读