如何访问 setState 回调之外的最新状态?

Posted

技术标签:

【中文标题】如何访问 setState 回调之外的最新状态?【英文标题】:how can I access the latest state outside of the setState callback? 【发布时间】:2021-06-01 12:24:29 【问题描述】:

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

非常感谢 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;

你可能会说我可以在setCount 回调中调用onIncrement,但是由于新的react 并发模式,你可以看到将来react 更新onIncrement 可能会被调用两次并输出两次结果,即不是想要的结果。

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

你已经可以(React 17 strict mode)看到onIncrement在开发模式和React并发模式成为默认后的特性中会被调用两次,所以在生产中可能会以这种方式调用两次。

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

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

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

谢谢!

【问题讨论】:

为什么不能在 setCount 回调中调用 onIncrement? 由于答案中解释的新并发模式,它可能在生产中被调用两次,并且它将在严格模式下被调用两次以避免将来出现错误。 啊抱歉,看错了那部分,实际上假设情况并非如此。似乎 React 再次完全进化了……你说“使用 ref 存储先前的状态不是解决方案”,但实际上这听起来是个好主意?你不想要裁判的理由是什么? 抱歉回复晚了。问题是,如果我选择将先前的状态存储在 ref 中,那么我将不得不在 useEffect 回调中调用 onIncrement 以访问最新状态,并且我将不得不使用另一个状态或 ref 来检测它点击事件是效果的原因,而不是改变计数状态的另一个事件或代码。当你有这么多这样的代码时,你的代码会被额外的 useState、useRef 和 useEffect 钩子污染。 由于increment 函数是由用户事件触发的,我想说count用户在单击按钮时看到的最新值。但我看到有些情况下需要“最新值”而不是“最新渲染值”,在这种情况下,“已知钩子”的所有组合都不合适。也许这值得向 React 团队提出一个问题,因为我认为这只能通过新的“引用状态”来解决 【参考方案1】:

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

如果我理解得很好,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 确实提供了保证,在刷新“上一次点击”之前,您不会获得“下一次点击”的渲染。这已经是并发模式的保证,但我们正在使它变得更强大——点击事件(以及类似的“有意”用户事件,如键盘输入)将始终刷新而不会中断微任务。在实践中,这意味着点击事件和其他故意事件永远不会像我们正在讨论的假设场景中那样被“忽略”。我们会在它稳定后将其包含在文档中。

谢谢丹和布赖恩

【讨论】:

这个帖子也启发了我在文档 issuetracker (github.com/reactjs/reactjs.org/issues/3560) 上的问题 ... 我认为 gaearons 评论“这已经是并发模式的保证,但我们正在使它变得更强大——点击事件(以及类似的“有意”用户事件,如键盘输入)将始终在微任务中不间断地刷新。实际上是整个故事中缺失的部分。

以上是关于如何访问 setState 回调之外的最新状态?的主要内容,如果未能解决你的问题,请参考以下文章

除了 setstate 颤振之外,如何在小部件构建中获取值

如何在 React 中访问功能组件中的最新状态值

更新后如何将函数传递给 SetState 回调? (反应)

react setState 回调没有更新状态

即使在 setState 回调中,解构状态也不会更新

ReactJS 状态未更新在 setState 回调中