我怎样才能访问 setState 回调之外的最新状态?

how can I access the latest state outside of the setState callback?

由于 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,但是由于新的反应并发模式,你可以看到在未来的反应更新中可能会调用 onIncrement两次并输出两次结果,这不是想要的结果。

The commit phase is usually very fast, but rendering can be slow. For this reason, the upcoming concurrent mode (which is not enabled by default yet) breaks the rendering work into pieces, pausing and resuming the work to avoid blocking the browser. This means that React may invoke render phase lifecycles more than once before committing, or it may invoke them without committing at all (because of an error or a higher priority interruption).

你已经(React 17 strict mode)看到onIncrement会在开发模式和React默认并发模式后的feature中被调用两次,所以在生产。

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

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

使用 useEffect 的另一个问题是我不确定这个事件处理程序 (onIncrement) 是效果的原因还是在另一个处理程序或 useEffect 回调中有状态变化造成的影响。 (存储额外的状态或 ref 以检测原因是多余的)

谢谢!

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

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

另一个让我感到困惑的来源是下面引自 'React docs' 的引述:

The commit phase is usually very fast, but rendering can be slow. For this reason, the upcoming concurrent mode (which is not enabled by default yet) breaks the rendering work into pieces, pausing and resuming the work to avoid blocking the browser. This means that React may invoke render phase lifecycles more than once before committing, or it may invoke them without committing at all (because of an error or a higher priority interruption).

我误解了,我认为 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 replying to me rising an issue in React's github repo. https://github.com/facebook/react/issues/20924

的回答

Dan Abramov 的另一个答案非常清楚:

I think one crucial nuance we're overlooking here is React will not actually do any of that. In particular, React does offer a guarantee that you will not get a render of the "next click" before the "previous click" has been flushed. This has already been a guarantee with Concurrent Mode, but we're making it even a stronger one — click events (and similar "intentional" user event like keyboard input) will always flush without interruption in a microtask. In practice, this means events for clicks and other intentional events will never get "ignored" as in this hypothetical scenario we're discussing. We'll include this in the documentation when it's stable.

谢谢丹和布赖恩