AbortController in React, cancel in function call

AbortController in React, cancel in function call

May 28, 2023

·

7 min read

先說這篇文章可能有點短,但我覺得滿實用的,所以還是來寫下紀錄,如果你有遇到一樣問題就…很棒,會簡單介紹一下我使用的背景,再來介紹什麼是 AbortController 最後是怎麼用在 React 專案上。

Background

最近開發了新的服務,但服務處於 MVP 階段就沒做太多的優化,發生的細節跟原因就不贅述了,反正就是 API 回應的時間特別的久,大概會需要 5 ~ 10 秒左右。

當每個 user 在等待 api 時,又不希望卡住 query 動作相關的 UI,所以當 user 在等待時去更新 select 或是 datePicker,就會導致同時 send 多個 api request,但每個都需要 5 ~ 10 秒回應,就有可能導致 race condition。

當下想到解法就是標題的 AbortController

What is AbortController

先簡短的說 AbortController 是讓我們可以透過 const abortItem = new AbortController() 並且透過回傳的 abortItem.signal object 以及 abortItem.abort function 控制 promise,如果還是不太懂,我們先看 MDN 介紹。

  • MDN 中文

AbortController 介面代表一個控制器物件,讓你可以在需要時中斷一個或多個 DOM 請求。

你可以使用 AbortController.AbortController() (en-US) 建立一個新的 AbortController 物件。與 DOM 請求溝通時則是使用 AbortSignal (en-US) 物件。

AbortController Browser compatibility

w3.org introduce AbortController

接下來是制定規範的 w3.org。

  • w3.org

Though promises do not have a built-in aborting mechanism, many APIs using them require abort semantics. AbortController is meant to support these requirements by providing an abort() method that toggles the state of a corresponding AbortSignal object. The API which wishes to support aborting can accept an AbortSignal object, and use its state to determine how to proceed.

APIs that rely upon AbortController are encouraged to respond to abort() by rejecting any unsettled promise with a new “AbortError” DOMException.

  • For web developer
controller = new AbortController()
  Returns a new controller whose signal is set to a newly created AbortSignal object.
controller . signal
  Returns the AbortSignal object associated with this object.
controller . abort()
  Invoking this method will set this object’s AbortSignal's aborted flag and signal to any observers that the associated activity is to be aborted.

w3.org 內文寫得非常的棒,很清楚的列出 AbortController interface 到底是長怎樣。

w3.org Aborting ongoing activities

How to use in React

首先簡單用法會是這樣,append 到 fetch 的第二個 arguments 上,調用 abort 就可以 cancel fetch function,你會在 browser 的 devtool network 的 status 上看到 canceled。

  • sample code
const abortItem = new AbortController();
fetch('https://jsonplaceholder.typicode.com/todos/1', { signal: abortItem.signal })
  .then(res => console.log(`res: ${res}`))
  .catch(err => console.log(`err: ${err}`));
abortItem.abort(); // err: AbortError: The user aborted a request.

useEffect with AbortController

那假設今天 API Call 是位於 React 的 useEffect 呢。我們可以把 abort 放到 useEffect 的 return 確保每次 useEffect 重新觸發都可以收回上個 fetch api call。

題外話這是 React 18 的範例,因為新版會在開發模式下重新觸發 useEffect,產生 warning 讓你知道你有 useEffect 的 fetch 沒處理到,但這可以透過關閉嚴格模式取消。

  • useEffect with AbortController
useEffect(() => {
  const abortItem = new AbortController();
  fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, { signal: abortItem.signal })
    .then(res => console.log(`res: ${res}`))
    .catch(err => console.log(`err: ${err}`));
  return () => abortItem.abort(); // err: AbortError: The user aborted a request.
},[id])

event function with AbortController

但最近開發上滿常會避免用過多的 useEffect 去 fetch api,通常只有 init data 用到,情境跟我不一樣,我要處理的是 fetch api call 被 event function 觸發。

首先想法就是當我 react component rerender 同時,要讓我能找到上一個 AbortController 的 reference,於是我們可以透過 ref 來儲存,因為更新 ref 本身也不會觸發 rerender 機制。

但一個 action 可能會有多個 function 並且交錯,要怎在一個 ref 記錄多個對應關係,就利用 Map 來儲存,於是寫了這個 hooks。

  • abortController hooks
function useAbortControllerRef() {
  const abortMap = useRef(new Map());

  const abortLastFetch = (key: string) => {
    const getAbortController = abortMap.current.get(key);
    if (getAbortController && typeof getAbortController.abort === 'function') {
      getAbortController?.abort();
    }
  };

  const fetchWithAbortController = (key: string) => {
    abortLastFetch(key); // abort previous request, if any
    const newController = new AbortController();
    abortMap.current.set(key, newController);
    return newController;
  };

  return {
    fetchWithAbortController,
  };
}

這個 hooks 會讓我們透過 fetchWithAbortController function 儲存 abortController 到對應的 key 上,每次 getData 執行時,就可以再度用 data1 找到並觸發 abort function,終止上一個 fetch api call。

  • using in component
function Comp() {
  const { fetchWithAbortController } = useAbortControllerRef();
  function getData(){
    const abortController = fetchWithAbortController('data1');
    fetch( apiUrl, {signal: abortController.signa} )
      .then()
      .catch();
    ...
  }
  return ...
}

實際上要不要 map 或是用 ref 其實不是很重要,你只要有辦法找到上一個 abortController 並且 call abort 就好。

心得

會寫下這篇是因為當下沒找到很方便的寫法,網路上幾乎都是 useEffect 的使用文章,原本以為可以再找 abortController 的同時,能更了解有沒有什麼更特殊的用法,看完 w3.org 就算死心了,簡單講就是 trigger promise reject,不過看完 w3.org 介紹後,對我也算獲益良多。

然後抱怨一下,原本想用 stable Diffusion 產生封面圖,每個都歪七扭怪地怪異,mid journey 又收回免費使用…,最後還是靠自己比較實在。

一樣最後感謝你的閱讀,如果有錯誤歡迎留言。