今天要來聊聊新手時期最痛恨的hook function — useEffect
。
首先,讓我們先來了解一下 useEffect
的誕生是為了處理怎樣的問題吧!有沒有這樣的情況,比如在前一篇那裡曾經介紹過的 search 功能,當我們設定的 state 經過更動後,想要讓他自動地跑一些其他連帶的 function。又或者更常見的 api 串接工作,幾乎要跟 useEffect
綁在一起了,在 React-query & SWR 還不普及的原生年代,幾乎大部分的教學都告訴你要用 useEffect
去打 api,但其中的原理大多要追朔到 class component 的作法,也就是整個 React 演化的歷史。
在早期 class component 的環境中都是以物件形式來區分 render 時機點,和前一篇模擬的範例很相似,也就是說所有渲染時機都有對應的位置和 function 可以去做更動,但缺點就是很攏長,那麼為了改善這個問題,於是 functional component 的出現解決了許多不便,但其連帶的 hooks function 設計理念也是從 class component 的使用情境下思考,並不像 solidjs 那樣重新設計避開 side effect 的使用問題,這也導致 useEffect
並不像其他 hooks 那樣單純,因為他本質上就是用來處理 side effect 的 hook。
從圖面可知,它是一個橫跨生命週期中三個渲染時機的hook function,所以自然比其他的hook function來得複雜,讓我們看看他的範例吧!
// 實際在component使用
useEffect(() => {
// 下面 function 對應的是 componentDidMount() 。
const timer = window.setInterval(() => {
setCount((pre) => pre + 1)
}, 1000)
// clean up function 對應的原本的 componentWillUnmount() 的地方
return () => window.clearInterval(timer)
// 第二個參數為 dependencies array,在陣列裡面的參數會使得 useEffect 去判斷該欄位的值是否需要更新,
// 如果需要則會依據該參數是否更新而觸發第一個帶入的 update function 而執行重新渲染。
}, [])
那麼,以上的基本概念是怎麼搞混的呢?讓我們試者加入常用情境 - 串接 api:
// 實際在component使用
useEffect(() => {
// 這裡是避免 react.18 二次渲染的解法,很多朋友都是直接處理 fetch(),
// 也就是沒有處理 clean up 的部分,那這裡就是處理fetch clean function 的做法
const controller = new AbortController();
const signal = controller.signal;
fetch(API_URL, {
signal: signal
})
.then((response) => response.json())
.then((response) => {
// 成功之後的處理
});
// 清除 request componentWillUnmounts
return () => controller.abort();
}, []);
看起來都能夠正常運行對吧!但如果我會因為 state change 而重新判斷他需不需要去重新取資料呢?如下:
// 實際在component使用
// const [params, setParams] = useState("")
// 上面的話不會有問題,但下面的作法就會有問題了
const [params, setParams] = useState({count: "", limit: ""})
useEffect(() => {
// 這裡是避免 react.18 二次渲染的解法,很多朋友都是直接處理 fetch(),
// 也就是沒有處理 clean up 的部分,那這裡就是處理fetch clean function 的做法
const controller = new AbortController();
const signal = controller.signal;
fetch(API_URL + `?count=${params.count}&limit${params.limit}`, {
signal: signal
})
.then((response) => response.json())
.then((response) => {
// 成功之後的處理
});
return () => controller.abort();
}, [params]);
// 這個模擬params change
// const onParamsChange = (e) => setParams(e.target.value); // 對應params為string
const onParamsCountChange = (e) => setParams((pre) => {...pre, count: e.target.value});
const onParamsLimitChange = (e) => setParams((pre) => {...pre, limit: e.target.value});
如果 params 只單純是字串的話,功能還不會有問題,但是如果是物件或是陣列格式的話就必須要注意了,因為在 dependencies array 裡面是採 shallow compare 的機制,簡單來說就是採 js 的比較機制,也就是新手常常搞混的地方,下面為簡單解釋:
5 === 5; // true
5 === 10; // false
true === true; // true
true === false; // false
"hello" === "hello"; // true
const hello = "hello"; // 更換指向
hello === "hello" // true
({a: "a"}) === { a: "a" }; // false,
// 因為 js 在物件型別的處理上是採 by reference,
// 所以即便 key & value 都相等也不等於相同物件
const obj = { a: "a" };
obj === obj; // true
obj === { a: "a" }; // false
[] === []; // false
// 相同的情況也發生在陣列上
[5] === [5]; // false
// 那react怎麼判斷dependencies array內部的否相等呢,如下:
const deepEqual = (old, new) => old.length === new.length && old.every((el, i) => el === new[i])
deepEqual([], []); // true
deepEqual([5], []); // false
deepEqual([5], [5]); //true
deepEqual([true], ["true"]); // false
deepEqual([hello], [hello]); // true
deepEqual([obj], [obj]); // true
const copyObj = obj;
deepEqual([obj], [copyObj]); // true
deepEqual([obj], [{ a: "a" }]); // false,因為 reference 是不同的
// 這就是很多使用上的錯誤盲區,它會判斷裡面的內容,但基本上仍然是參照 js 的判斷機制
// 所以在 dependencies array 內部,必須盡量避開 by reference 的判斷情境
// 如果是物件看能不能往下取一層就好,
// 如果必要用到物件或陣列整個來判斷你的 updating function 是否需要執行的話,
// 考慮採用 useMemo 或 useCallback 額外做處理。
那麼讓我們再回頭修正一下之前的問題吧!
// 實際在component使用
const [params, setParams] = useState({count: "", limit: ""})
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
if(API_URL) { // 這裡我加了判斷,避免參數沒有接到fetch會失敗
fetch(API_URL + `?count=${params.count}&limit=${params.limit}`, {
signal: signal
})
.then((response) => response.json())
.then((response) => {
// 成功之後的處理
});
return () => controller.abort();
}
}, [API_URL, params.count, params.limit]);
// 這裡就不能只擺 params 會有剛才提到的問題
// 必須往下取到 by value 的值
// 這個模擬params change
const onParamsCountChange = (e) => setParams((pre) => {...pre, count: e.target.value});
const onParamsLimitChange = (e) => setParams((pre) => {...pre, limit: e.target.value});
在以上的講解之後,希望大家在使用的同時記住以下幾點:
- 不要忘記你的 dependencies array 的存在。
- 要注意你放進 dependencies array 中的參數型別,如果是 by reference 的資料(ex: object, array…), 要記得額外用 useMemo 或 useCallback 來處理。
- 如果不想看到 React 18 渲染兩次的話,記得要處理 clean up function。
那麼今天的分享就到這裡,下一篇我們來聊聊useRef吧!
useFootgun meme
生命週期圖片1
生命週期圖片2
Jack Herrington Medium