React-這個 fetch 我剛剛要但現在又不要了

有夠任性。

簡述

這篇其實是要介紹兩個東西:

  1. clean function(useEffect) 的適用時機
  2. AbortController

說真得我還蠻少用到這兩樣東西的,所以想記錄一下。

範例

假設有一個 Component 的內容是透過 useEffect 來從 API 取得資料的。那麼「如果我在 fetch 的期間就把 Component unmount 的話」,會發生什麼事?

這邊先看一段 code,會比較好懂這是什麼意思:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Button from "Button";
import { useState } from "react";
import TripList from "TripList";

export default function App() {
// 控制 List 是否顯示的 state
const [showTripList, setShowTripList] = useState(true)
return (
<div">
<h2>Trip List</h2>
<button onClick={() => setShowTripList(false)}>Cancel fetching</button>
{showTripList && <TripList />}
</div>
);
}

這邊不用管 <TripList /> 是什麼,只要知道他會用 useEffect 來打 API 拿資料就好。

至於 <Button> 的作用是把 <TripList /> 從畫面上移除,也就是前面說的 unmount。

所以現在的情況是,如果我在 <TripList /> 還在 fetch 的期間就按下 <Button /> 的話,會發生什麼事?

沒意外的話你應該會得到這個訊息:

Warning: Can’t perfrom a React state update on an unmounted component. This is a no-op, …..

附註:後來的 React 似乎有做一點調整,所以不會看到這個訊息。

他的意思是說,「我現在要更新 state,但那個 Component 已經不存在了,安捏嘎丟?」

我們可以先思考一下整個流程,就會比較清楚為什麼會是這種結果了:

  1. 一開始 showTripList 為 true,所以會渲染 <TripList />
  2. <TripList /> 開始 fetch 資料
  3. 按下 <Button /><TripList /> 從畫面上移除掉(unmount)
  4. fetch 回來了,更新原本用來儲存資料的 state
  5. 發現 Component 不存在,所以顯示 Warning

這邊理解後,就可以繼續往下思考該怎麼解決了。

要怎麼在元件 unmount 的時候把 fetch 取消?

這個是最核心的問題,所以才刻意用標題來醒目。不過我其實在一開始就有先洩漏了,就是透過 clean functionAbortController 來實現。

要 trigger clean function 的方式很簡單,就是「在 useEffect 裡面回傳一個 function」,像這樣:

1
2
3
4
5
6
7
8
useEffect(() => {
...
...
...

// clean function
() => {...}
}, [])

而要取消 fetch 的方式就是用 AbortController 來做連結。

直接來看範例吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { useEffect, useState } from "react";
const API = "https://reqres.in/api/users?page=1";
const sleep = (ms) => new Promise((resolve) => setTimeout(() => resolve(), ms));

export default function TripList() {
const [data, setData] = useState([]);
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);

useEffect(() => {
// 建立 instance
const controller = new AbortController();
const fetchData = async () => {
setIsPending(true);
setError(null);
try {
await sleep(1000);
// 用 signal 把 fetch 和 controller 串連起來
const response = await fetch(API, { signal: controller.signal });
if (!response.ok) throw new Error(response.statusText);

const json = await response.json();
setData(json.data);
} catch (err) {
// For developer
console.log("👉 Exception message: ", err.message);
// 在 AbortError 時不要更新 state
err.name === "AbortError"
? console.log("User abort the fetch")
: setError("Can not fetch the data");
} finally {
setIsPending(false);
}
};
fetchData();
// unmount 時就執行這個 clean function
return () => controller.abort();
}, []);

return ...;
}

改成這樣以後,如果在 fetch 的狀態下點了 <Button />,就會把 <TripList /> 給移除掉,執行 () => controller.abort() 把 fetch 給取消掉。

取消的同時會拋出一個 AbortError 的 Exception,所以會進到 cache 區塊。但要注意,請務必記得多做一層「是不是 AbortError」的判斷,為什麼?

因為我們就是希望 Abort 的情況下不要更新 state,所以才要檢查 Error 的型別,把它跟一般的錯誤處理區別開來。

總而言之,如果還是似懂非懂的話,可以到我寫的 範例 來試試看。希望之後能善用這項技術寫出更好的 code。

再來談談-淺拷貝與深拷貝的雷 React-用 portal 把元件移到我想要的位置
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×