先是在前端社群上看到有人在問問題,為什麼他在開發時,useEffect(callback,[])
,callback 會執行兩次,才看到有人貼上 React 18 beta 的文件,裡面提到為什麼 React 18 會修改成在嚴格模式下執行兩次。
但我看到其中有篇標題 You Might Not Need an Effect
最吸引我,文章很長很長,官方直接給了滿多正確使用 useEffect 的情境時機,甚至包含使用指引。
所以就想寫一篇文章,先從 React 18 useEffect 兩次 callback 帶來什麼影響,後面再來介紹 You Might Not Need an Effect 寫了些什麼吧。
React 18 beta document: You Might Not Need an Effect
What is useEffect
文件上對於 useEffect 定義是 The Effect Hook lets you perform side effects in function components
,顧名思義就是 function component 的 side effect,我自己也偏向這個說法,拋棄掉過去的 componentDidMount 等等的 lifecycle 的想法。
useEffect 使用上就三個參數,依照下方介紹分為 callback 跟 cleanCallback 以及 dependency array。
- useEffect 參數
useEffect(
callback();
return () => cleanCallback();
,[arg1, ....])
// dependency array, dependency array is depending on shallow equal compare
- useEffect 執行順序
* React mounts the component.
* Effect effects are created
* React update the component. if arg1 compared the previous is true.
* Effect cleanCallback code runs and callback runs.
* React unmounts the component.
* Effect cleanCallback code runs.
React 18 simulates useEffect
順道提一開始的問題,為什麼 useEffect 會執行兩次吧,下方是 React useEffect 在 React 18 strict mode 下,useEffect 是如何被調用的。
- Component render step on strict mode react 18
* React mounts the component.
* Effect effects are created.
* React simulates effects being destroyed on a mounted component.
* Effects are destroyed.
* React simulates effects being re-created on a mounted component.
* Effect setup code runs
這個 flow 只會在 development 下開啟 strict mode 執行,react 希望透過快速的執行 useEffect cleanup,並且 重新使用新的 state,這是為了確保我們有正確的回收 component useEffect,
React Document: ensuring-reusable-state
React github issue: dan abramov reply
Demo codesandbox: useEffect on react 17 、 useEffect on react 18
how to solve the twice callback problem
如果你不想關閉 strict mode 的話,又想避免 callback 被調用兩次的話,答案是無解…。
如果你在 useEffect fetch api 的話,只能依賴變數去做為 setState 的 throttle,避免顯示 setState on unmounted 下方的錯誤。或用其他 state manager 去存取資料,更或是用 AbortController 去回收 api request。
- unmounted and stop setState
useEffect(() => {
let stop = false;
fetch(url).then(res => {
if (!stop) {
setList(res);
}
})
return () => { stop = true }
}, []);
但我個人是認為,我自己是會視而不見,因為在開發時,就應該完全清楚知道整個的 data flow、component unmounted 的時機點,以及 state 是會在什麼時機被回收的。開發上要注意重複 callback 會不會有問題即可。
When we should use useEffect
回到一開始的重點,根據 You Might Not Need an Effect 的文件介紹,下面是結論,關於判斷何時該使用 useEffect 的結論。
- you-might-not-need-an-effect recaps (中文是我翻譯的,很直翻…)
- If you can calculate something during render, you don’t need an Effect.
( 如果你可以在 render 時,計算出某些值,那你不需要 Effect。 )
- To cache expensive calculations, add useMemo instead of useEffect.
( 當你需要快取耗費效能的計算,請使用 useMemo 而不是 useEffect )
- To reset the state of an entire component tree, pass a different key to it.
( 要重置這個 component tree 中的 state,傳遞 key 到 component 來達到 )
- To reset a particular bit of state in response to a prop change, set it during rendering.
( 要重置特定的 prop 改變來自 response 特定的 state,那就設定這個 state 在 rendering 時。 )
- Code that needs to run because a component was displayed should be in Effects, the rest should be in events.
( code 需要執行,是因為來自顯示時的副作用,那會需要用 Effect,其餘都會是在 event。 )
- If you need to update the state of several components, it’s better to do it during a single event.
( 如果需要更新 state 是在各個不同的 components 時,比較好的做法是在單一 event 處理 )
- Whenever you try to synchronize state variables in different components, consider lifting state up.
( 不論你是不是要嘗試在不同 components 同步 state,考慮把 state 拉到上層。 )
- You can fetch data with Effects, but you need to implement cleanup to avoid race conditions.
( 你可以使用 useEffect fetch 資料,但你需要處理回收邏輯,來避免 race condition。 )
我自己收斂的結論是,頁面的 render event 皆用 useEffect,類似 ga track
、 websocket
,剩下就盡量使用 event 處理,當你發現 event 不符合 DRY 邏輯,可能就是要搬移到 useEffect。
文中很多在強調請先不要用 useEffect、不過度優化效能,真的有出現問題才回頭考慮用 useEffect。
- unnecessary Effect
function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// Avoid: redundant state and unnecessary Effect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}
Use case: typing search input
內文我覺得最棒的例子,是一個關於 page
、search
的更新,我們直接看 sample code 比較快。
- useEffect handle page and query
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
let ignore = false;
fetchResults(query, page).then(json => {
if (!ignore) {
setResults(json);
}
});
return () => {
ignore = true;
};
}, [query, page]);
function handleNextPageClick() {
setPage(page + 1);
}
// ...
}
假設你只用 event 處理更新列表的話,你在 載入頁面、上一頁、下一頁,一樣也要用 useEffect 處理 fetch 資料,還可以在這邊針對每次的 fetch response callback 做控制。
為什麼我們會需要 ignore 來判斷 setResult,這是因為有可能會出現 race condition 的問題,當你今天連續的更新 query,
假設 query 可以透過 input 更新,當快速的輸入 input,就會觸發多次 fetch,假設今天前面的請求比後面還慢,那就代表我們 前面的 setState 執行會慢於後面的,就導致資料對不上當前 input 的 query。
這情境用 useEffect 會明顯好閱讀許多,集中管理資料的進入點。雖然你硬要用 onClick 處理也不是不行,就是散落邏輯不容易維運。
- race condition ( if user want to query apple )
1. typing `app`.
2. fetch api with `app` and waiting api response.
3. continue typing `apple`.
4. fetch api with `apple` and waiting api response.Somehow fetch api `apple` response faster than before.
6. setState as `apple` response.
7. fetch api `app` response.
8. setState as app response.
結論
開發時不要預想用 useEffect 處理,但如果 code 難以閱讀、管理,或是要避免每次 update 都執行的 function,那才來考慮使用 useEffect,如果還是聽不懂我在說啥毀,可以回上面的 recap 再看一遍,又或是把 React 18 的文章看一看吧。
心得
我自己看完整篇 React 18,是真心覺得真的很棒,推薦大家都要看一下,連面試時候我都跟面試者推薦看一下,因為內容真的太棒了。雖然滿多觀念你可能都知道,但如果能多少學到一點點東西,我覺得都算超值了。
公司有一個專案,算是沒有好好的使用 useEffect,導致 useEffect 之間,互相 depenency,一個不注意就會長這樣,所以我算是對 useEffect 感觸許多。恩…,見賢思齊,見不賢而內自省也。
過去 React 都是給我們很多工具,但很少會跳出來說,怎樣寫得完美準則,這點也符合我喜歡 React 的原因,大家常形容寫 React 就像是在大海游泳,沒有給你很明確的方向,可以寫出糞 code,但同時又可以寫出很優美的 code
,完全看你對 React 的熟悉掌握度如何。
ps. 稍微 murmur 一下,文章兩個重點,反倒讓我極度難收斂…,很久沒打文章,表達能力真的變差了,還是看不懂我在說啥的,就直接看 React 18 文章吧 XDD。
React 18 beta document: You Might Not Need an Effect