React 的那些事 — useEffect
今天要來聊聊新手時期最痛恨的hook function — useEffect。
首先,讓我們先來了解一下 useEffect 的誕生是為了處理怎樣的問題吧!
有沒有這樣的情況,比如在 useState 那裡曾經介紹過的 search 功能,當我們設定的 state 經過更動後,想要讓他自動地跑一些其他連帶的 function。
又或者更常見的 api 串接工作,幾乎要跟 useEffect 綁在一起了,在 React-query & SWR 還不普及的原生年代,幾乎大部分的教學都告訴你要用 useEffect 去打 api,但其中的原理大多要追朔到上個世紀的 class component 作法,也就是整個 React 演化的歷史。
很多初學的新手,在對 javascript 的理解上並不熟悉,在早期 class component 的環境中都是以物件形式來區分 render 時機點,也就是說所有渲染時機都有對應的位置和 function 可以去做更動,但缺點就是很攏長,也顯得不符合大多數純渲染狀態的 component 的製作。
下圖為常見的教學會講到的渲染時機圖(圖片出處):
於是 functional component 的出現解決了許多不便,但其連帶的 hooks function 設計理念也是從 class component 的使用情境下思考,並不像 solid 那樣重新設計避開 side effect 的使用問題,這也導致 useEffect 並不像其他 hooks 那樣單純,因為他本質上就是用來處理 side effect 的 hook。
下圖為hook function對應的生命週期(圖片出處):
從圖面可知,它是一個橫跨生命週期中三個渲染時機的hook function,所以自然比其他的hook function來得複雜,讓我們看看他的範例吧!
useEffect(() => {
// update function對應的是component updating。
const timer = window.setInterval(() => {
setCount((pre) => pre + 1)
}, 1000)
// clean up function 對應的原本的 componentWillUnmount() 的地方
return () => window.clearInterval(timer)
// 第二個參數為 dependencies array,在陣列裡面的參數會使得 useEffect 去判斷該欄位的值是否需要更新,
// 如果需要則會依據該參數是否更新而觸發第一個帶入的 update function 而執行重新渲染。
// 如果不帶的話會造成 error,這邊我就不多做解釋了
}, [])
那麼,以上的基本概念是怎麼搞混的呢?讓我們試者加入常用情境 — 串接 api:
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) => {
// 成功之後的處理
});
return () => {
// 清除 request componentWillUnmounts
controller.abort();
};
}, []);
看起來都能夠正常運行對吧!但如果我會因為 state change 而重新判斷他需不需要去重新取資料呢?如下:
// 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 + params, {
signal: signal
})
.then((response) => response.json())
.then((response) => {
// 成功之後的處理
});
return () => {
// 清除 request componentWillUnmounts
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 額外做處理。
那麼讓我們再回頭修正一下之前的問題吧!
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});
注意事項
1. 不要忘記 dependencies array 的存在。
2. 要注意你放進 dependencies array 中的參數型別,如果是 by reference 的資料(ex: object, array…), 要記得額外用 useMemo 或 useCallback 來處理。
3. 如果不想看到 React 18 渲染兩次的話,記得要處理 clean up function。
以上大概就是我所能分享的部分了,希望能幫助到大家,很多時候在學習 React 的過程中會覺得很難的原因,其實是 javascript 的問題,當然也有一些後起之秀 solidjs, svelte, preact…,能處理得很方便,並且優化了很多 mounting & unmounting 的問題,但源頭也都是吸收 React 去改良,所以能多了解一些,再跳去新興框架的時候自然會覺得容易許多。