本篇通譯自 https://blog.devgenius.io/how-to-better-poll-apis-in-react-312bddc604a4
作者: Zachary Lee
替代 setInterval
境蜕,間隔調(diào)用異步方法的更好解決方案
在 Web 開發(fā)中微峰,我們可能需要不斷地輪詢后端 API 以獲取頁面上要更新的最新數(shù)據(jù)孕荠。雖然 WebSocket 是更好的選擇带饱,但在某些情況下輪詢也可以邻奠。
那么如何在 React 中做到這一點呢酌儒?
setInterval
我們可以使用 setInterval 連續(xù)執(zhí)行 async 方法辜妓,這可能是最簡單的解決方案。
const App = () => {
const [origin, setOrigin] = useState('');
const updateState = useCallback(async () => {
const response = await fetch('https://httpbin.org/get');
const data = await response.json();
setOrigin(data?.origin ?? '');
}, []);
useEffect(() => {
setInterval(updateState, 3000);
}, [updateState]);
return <main>{`Your origin is: ${origin}`}</main>;
};
但是這個解決方案有一些問題忌怎。首先setInterval是不準(zhǔn)確的, 二是粒度不易控制籍滴,造成浪費。例如榴啸,對于一個響應(yīng)時間較長的 API 請求孽惰,上一次響應(yīng)的內(nèi)容在頁面上還沒有更新,下一個請求會重新發(fā)送鸥印。
這并不理想勋功。
setTimeout + async…await
我們可以使用setTimeoutand
async
...await
來實現(xiàn)一個自定義鉤子:
const useIntervalAsync = (fn: () => Promise<unknown>, ms: number) => {
const timeout = useRef<number>();
const run = useCallback(async () => {
await fn();
timeout.current = window.setTimeout(run, ms);
}, [fn, ms]);
useEffect(() => {
run();
return () => {
window.clearTimeout(timeout.current);
};
}, [run]);
};
接下來,它是這樣使用的:
const App = () => {
const [origin, setOrigin] = useState('');
const updateState = useCallback(async () => {
const response = await fetch('https://httpbin.org/get');
const data = await response.json();
setOrigin(data?.origin ?? '');
}, []);
useIntervalAsync(updateState, 3000);
return <main>{`Your origin is: ${origin}`}</main>;
};
此解決方案使用async
...await
和setTimeout
來確保異步任務(wù)在上一個異步任務(wù)完成后再執(zhí)行
但是異步任務(wù)總是很棘手库说。想象一個案例:如果使用這個鉤子的組件在等待異步響應(yīng)時被卸載狂鞋,那么這個定時任務(wù)將一直在后臺運行。這是一個嚴(yán)重的錯誤潜的,我們可以通過記錄掛載狀態(tài)來避免這種情況骚揍。
const useIntervalAsync = (fn: () => Promise<unknown>, ms: number) => {
const timeout = useRef<number>();
const mountedRef = useRef(false);
const run = useCallback(async () => {
await fn();
if (mountedRef.current) {
timeout.current = window.setTimeout(run, ms);
}
}, [fn, ms]);
useEffect(() => {
mountedRef.current = true;
run();
return () => {
mountedRef.current = false;
window.clearTimeout(timeout.current);
};
}, [run]);
};
可以通過記錄mountedRef
. 很簡單,對夏块,但其實很實用疏咐,當(dāng)然你也可以把mounted
狀態(tài)做成自定義hook
,方便復(fù)用脐供。例如useMountedState
下面:
import { useCallback, useEffect, useRef } from 'react';
const useMountedState = () => {
const mountedRef = useRef(false);
const getState = useCallback(() => mountedRef.current, []);
useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
};
}, []);
return getState;
};
export default useMountedState;
復(fù)雜案例
在一些比較復(fù)雜的情況下浑塞,我們可能需要主動更新頁面信息。例如政己,在一些交互之后酌壕,我希望updateState
立即調(diào)用以更新頁面上的最新數(shù)據(jù)掏愁。
當(dāng)然我可以updateState
直接調(diào)用,但是下一個定時任務(wù)可能會執(zhí)行的很快卵牍,很浪費果港。所以我們可以添加一些特性來useIntervalAsync
讓它支持刷新。
import { useCallback, useEffect, useRef } from 'react';
const useIntervalAsync = <R = unknown>(fn: () => Promise<R>, ms: number) => {
const runningCount = useRef(0);
const timeout = useRef<number>();
const mountedRef = useRef(false);
const next = useCallback(
(handler: TimerHandler) => {
if (mountedRef.current && runningCount.current === 0) {
timeout.current = window.setTimeout(handler, ms);
}
},
[ms],
);
const run = useCallback(async () => {
runningCount.current += 1;
const result = await fn();
runningCount.current -= 1;
next(run);
return result;
}, [fn, next]);
useEffect(() => {
mountedRef.current = true;
run();
return () => {
mountedRef.current = false;
window.clearTimeout(timeout.current);
};
}, [run]);
const flush = useCallback(() => {
window.clearTimeout(timeout.current);
return run();
}, [run]);
return flush;
};
export default useIntervalAsync;
可以看到我們已經(jīng)添加了flush
方法糊昙。它的內(nèi)部邏輯是取消下一個定時任務(wù)辛掠,run
直接執(zhí)行該方法。
但是我們添加了一個runningCount
释牺,這是為了什么萝衩?
想象一個案例:當(dāng)鉤子在內(nèi)部run以正常的邏輯間隔執(zhí)行函數(shù)時,而異步響應(yīng)正在等待解決没咙,flush被外部調(diào)用以期望立即執(zhí)行猩谊,然后run將再次執(zhí)行。這是因為最后一個run還沒有解決祭刚,最新的計劃任務(wù)還沒有創(chuàng)建牌捷,所以不能取消。
也就是說涡驮,run此時有兩個函數(shù)正在執(zhí)行暗甥,雖然這并不關(guān)鍵,但是如果我們?nèi)匀皇褂们懊娴倪壿嬜酵保敲催@兩個run在解決后會創(chuàng)建兩個定時任務(wù)淋袖。這會導(dǎo)致更大的浪費。
所以我們可以使用runningCount
來記錄當(dāng)前的執(zhí)行次數(shù)锯梁,并在函數(shù)中保證只有在是下一個任務(wù)時才創(chuàng)建新的定時任務(wù)。runningCount 0
另外焰情,通過 TypeScript 的泛型陌凳,我們可以很方便的包裝原有的函數(shù)以適應(yīng)更多的情況。一個簡單的例子:
const App = () => {
const [origin, setOrigin] = useState('');
const updateState = useCallback(async () => {
const response = await fetch('https://httpbin.org/get');
const data = await response.json();
setOrigin(data?.origin ?? '');
}, []);
const update = useIntervalAsync(updateState, 3000);
return (
<main>
<div>{`Your origin is: ${origin}`}</div>
<button onClick={update}>update</button>
</main>
);
};
本文由mdnice多平臺發(fā)布