先說這篇文章可能有點短,但我覺得滿實用的,所以還是來寫下紀錄,如果你有遇到一樣問題就…很棒,會簡單介紹一下我使用的背景,再來介紹什麼是 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 又收回免費使用…,最後還是靠自己比較實在。
一樣最後感謝你的閱讀,如果有錯誤歡迎留言。