如果有個算法是2秒以上很耗時的長任務,希望在執行長任務前後修改state渲染loading畫面,可能會難以達到預期效果,會看到loading畫面一閃而過。
setLoading(true);
longTask();
setLoading(false);
因為React在setstate後狀態不會馬上改變與渲染,如果後面緊跟一個長任務,長任務會馬上佔用所有資源,直到結束後才改變狀態與渲染。
實現的思路是在setState時建立一個Promise,然後useEffect監聽到狀態改變後呼叫這個Promise的resolve方法,直接上程式碼。
const Main = () => {
const [spinning, setSpinning] = useState(false);
// 防止useEffect取到的spinning和setSpinningAsync裡取到的不同
const spinningStateRef = useRef(false);
// 儲存Promise的resolve方法
const spinningResolveRef = useRef(() => {});
const setSpinningAsync = (isSpinning) => {
// 先呼叫與清空上次的resolve,避免有地方程式被await卡住
spinningResolveRef.current();
spinningResolveRef.current = () => {};
const currentLoadingState = spinningStateRef.current;
if (isSpinning == currentLoadingState) return;
return new Promise((resolve) => {
setSpinning(isSpinning);
// setState後儲存這次Promise的resolve
spinningResolveRef.current = resolve;
});
};
useEffect(() => {
const updateSpinningRef = async () => {
// 如果電腦很卡,可以多設些緩衝時間確保畫面已渲染
await new Promise((resolve) => setTimeout(resolve, 100));
// 確保狀態已改變,畫面已渲染,就呼叫resolve(),讓executWithAsync函式await的程式繼續
spinningResolveRef.current();
spinningStateRef.current = spinning;
};
updateSpinningRef();
}, [spinning]);
const executWithAsync = async () => {
await setSpinningAsync(true);
longSyncTask(2000);
await setSpinningAsync(false);
};
return (
<Spin spinning={spinning}>
<Button onClick={executWithAsync}>Wait until spinning</Button>
</Spin>
);
};