组件在异步 React 上下文更新时呈现两次
Component renders twice on async React context update
我有一个使用 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 通过采取一系列操作来解决这个问题,但我很好奇是否可以避免它。
除非这会导致性能问题,否则我建议不要担心额外的渲染。我发现 this post about fixing slow renders before worrying about re-renders 对这个主题很有帮助。
然而,看起来 React 确实有一个 不稳定的 (所以不应该使用它) API批处理更新 ReactDOM.unstable_batchedUpdates
。但是,如果它被删除或更改,使用它可能会在将来引起麻烦。但是要回答你的问题"can we somehow batch updates?",是的。
const asyncInc = async () => {
await delay(1000);
ReactDOM.unstable_batchedUpdates(() => {
setCounter(counter + 1);
setCounter(counter + 1);
});
};
基本上,您看到的 syncInc()
是 的点击事件。因此,您只会看到它渲染一次。
React batches all setStates
done during a React event handler, and applies them just before exiting its own browser event handler.
对于您的 asyncInc()
,它超出了事件处理程序的范围(由于 async
),因此预计您会得到两次重新呈现(即不批处理状态更新) .
Can, we somehow batch updates?
是的,React 可以 batch updates 在异步函数中。
我有一个使用 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 通过采取一系列操作来解决这个问题,但我很好奇是否可以避免它。
除非这会导致性能问题,否则我建议不要担心额外的渲染。我发现 this post about fixing slow renders before worrying about re-renders 对这个主题很有帮助。
然而,看起来 React 确实有一个 不稳定的 (所以不应该使用它) API批处理更新 ReactDOM.unstable_batchedUpdates
。但是,如果它被删除或更改,使用它可能会在将来引起麻烦。但是要回答你的问题"can we somehow batch updates?",是的。
const asyncInc = async () => {
await delay(1000);
ReactDOM.unstable_batchedUpdates(() => {
setCounter(counter + 1);
setCounter(counter + 1);
});
};
基本上,您看到的 syncInc()
是
React batches all
setStates
done during a React event handler, and applies them just before exiting its own browser event handler.
对于您的 asyncInc()
,它超出了事件处理程序的范围(由于 async
),因此预计您会得到两次重新呈现(即不批处理状态更新) .
Can, we somehow batch updates?
是的,React 可以 batch updates 在异步函数中。