如何在 React 中更好地輪詢 API贪绘?

本篇通譯自 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...awaitsetTimeout來確保異步任務(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ā)布

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末内舟,一起剝皮案震驚了整個濱河市合敦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌验游,老刑警劉巖充岛,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異耕蝉,居然都是意外死亡崔梗,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門垒在,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蒜魄,“玉大人,你說我怎么就攤上這事√肝” “怎么了旅挤?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長伞鲫。 經(jīng)常有香客問我粘茄,道長,這世上最難降的妖魔是什么秕脓? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任柒瓣,我火速辦了婚禮,結(jié)果婚禮上撒会,老公的妹妹穿的比我還像新娘嘹朗。我一直安慰自己,他們只是感情好诵肛,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布屹培。 她就那樣靜靜地躺著,像睡著了一般怔檩。 火紅的嫁衣襯著肌膚如雪褪秀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天薛训,我揣著相機與錄音媒吗,去河邊找鬼。 笑死乙埃,一個胖子當(dāng)著我的面吹牛闸英,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播介袜,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼甫何,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了遇伞?” 一聲冷哼從身側(cè)響起辙喂,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鸠珠,沒想到半個月后巍耗,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡渐排,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年炬太,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片驯耻。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡娄琉,死狀恐怖次乓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情孽水,我是刑警寧澤票腰,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站女气,受9級特大地震影響杏慰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜炼鞠,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一缘滥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谒主,春花似錦朝扼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至观游,卻和暖如春搂捧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背懂缕。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工允跑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人搪柑。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓聋丝,卻偏偏與公主長得像,于是被迫代替她去往敵國和親工碾。 傳聞我的和親對象是個殘疾皇子潮针,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容