组件在异步React上下文更新中呈现两次

时间:2019-10-03 23:40:40

标签: reactjs react-hooks react-context react-state-management react-state

我有React应用程序,它使用React上下文管理状态。我已经创建了简单的counter incrementation reproduction

有两个上下文。一个用于存储状态,第二个用于调度。这是从article中提取的一种模式。

状态上下文仅存储一个数字,并且只能对此状态调用一个动作:

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "inc": {
      return state + 1;
    }
  }
}

我还有两个辅助函数,用于同步和异步增加值:

async function asyncInc(dispatch: React.Dispatch<Action>) {
  await delay(1000);
  dispatch({ type: "inc" });
  dispatch({ type: "inc" });
}

function syncInc(dispatch: React.Dispatch<Action>) {
  dispatch({ type: "inc" });
  dispatch({ type: "inc" });
}

这是在组件中使用它的方式:

const counter = useCounterState();
const dispatch = useCounterDispatch();

return (
  <React.Fragment>
    <button onClick={() => asyncInc(dispatch)}>async increment</button>
    <button onClick={() => syncInc(dispatch)}>sync increment</button>
    <div>{counter}</div>
  </React.Fragment>
);

现在,当我单击sync increment按钮时,一切都会按预期进行。它将调用inc操作两次,将计数器增加2,并且仅执行一次组件的重新渲染。

当我单击async increment按钮时,它将首先等待一秒钟并执行inc操作两次,但是它将重新渲染组件两次。

您必须打开控制台才能查看来自渲染组件的日志。

我有点理解为什么。当它是同步操作时,所有事情都在组件渲染期间发生,因此它将首先操纵状态并在最后渲染。当它是异步操作时,它将首先渲染组件,一秒钟后它将更新状态,一旦触发重新渲染,它将第二次更新,从而触发下一次重新渲染。

那么有可能仅执行一次重新渲染就执行异步状态更新吗?在相同的复制中,有类似的示例,但是使用React.useState时也存在相同的问题。

可以,我们以某种方式批量更新吗?还是我必须创建另一个动作,以便一次对状态执行几项操作?

我还创建了this reproduction,该问题可以通过采取一系列措施来解决此问题,但我很好奇是否可以避免。

2 个答案:

答案 0 :(得分:2)

基本上,您为syncInc()看到的点击事件为batching。因此,您只能看到它呈现一次。

  

React 批处理在React事件处理程序中完成的所有setStates ,并在退出其自己的浏览器事件处理程序之前应用它们。

对于您的asyncInc(),它不在事件处理程序的范围内(由于async),因此,预计您将获得两次重新渲染(即不进行批处理状态更新)。 / p>

  

我们可以批量更新吗?

是的,React可以在异步函数中batch updates

答案 1 :(得分:1)

除非这会导致性能问题,否则我建议不要担心其他渲染。我发现this post about fixing slow renders before worrying about re-renders对这个主题有帮助。

但是,React看起来确实具有不稳定 (因此不应使用) API,用于批量更新ReactDOM.unstable_batchedUpdates。但是,如果删除或更改它,将来可能会引起头痛。但是要回答您的问题“我们能以某种方式批量更新吗?” ,是的。

const asyncInc = async () => {
  await delay(1000);

  ReactDOM.unstable_batchedUpdates(() => {
    setCounter(counter + 1);
    setCounter(counter + 1);
  });
};