React 的第三個 hook:useEffect

據說是最難理解的一個。

基本概念

React render 完,瀏覽器 paint 出畫面以後,你想做什麼?

可以想成是 render 完以後的 callback function,你可以在裡面寫要做的事情。

一樣拿 todo list 來舉例,假設我希望有 localstorage 的功能,直覺的做法可能是這樣子:

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
42
43
44
45
46
47
48
// 因為 setTodos 是非同步的,所以沒辦法直接用 setItem(todos) 的方式來存
// 而是要跟改 state 的方式一樣,產生一個新的 state 來存
const setLocalStorage = (todos) =>
window.localStorage.setItem("todos", JSON.stringify(todos));

// 新增 todos
const handleAddTodo = (content) => {
setTodos([
{
id,
content,
isDone: false,
},
...todos,
]);
// 更新 localstorage
setLocalStorage([
{
id,
content,
isDone: false,
},
...todos,
]);
id++;
};
// 刪除 todos
const handleRemoveTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
// 更新 localstorage
setLocalStorage(todos.filter((todo) => todo.id !== id));
};
// 編輯 todos
const handleToggleTodoState = (id) => {
setTodos(
todos.map((todo) => {
if (todo.id !== id) return todo;
return { ...todo, isDone: !todo.isDone };
})
);
// 更新 localstorage
setLocalStorage(
todos.map((todo) => {
if (todo.id !== id) return todo;
return { ...todo, isDone: !todo.isDone };
})
);
};

就是在每個會「改變 todos 的 state 的地方」都加上儲存紀錄的處理。不過這樣不就跟寫 Vanilla JS 沒兩樣了嗎?所以其實有更好的做法,就是透過 useEffect

1
2
3
useEffect(() => {
window.localStorage.setItem('todos', JSON.stringify(todos));
});

對,就是這麼煞氣的用一行來輕輕鬆鬆解決。但重要的是要知道為什麼可以這樣做?

這是因為 useEffect 的執行時間是在每一次 render 完以後才被執行。所以這整個流程是這樣子:

  1. 第一次載入,render 完以後,把目前 todo 的 state 儲存到 localStorage
  2. 新增一筆 todo,更新了 state,觸發 re-render
  3. 重新 render 完,把畫面 paint 出來後,再次執行 useEffect
  4. useEffect 根據目前 todo 的 state 把東西儲存到 localStorage

所以你知道原因了嗎?要 render 東西以前必須先有 state,而 useEffect 是在 render 完以後才被執行,那不就代表在 useEffect 裡的 state 一定是目前畫面上最新的狀態嗎?

所以在這個時候去設定 storage 絕對不會出錯,而且也是個很不錯的時機。

關於 useEffect 的第二個參數,為什麼需要它?

附註:這個參數叫做「dependencies」

假設一個 Component 中有多個 state,而在用 useEffect 時又沒有指定「第二個參數」,告訴 React 你要觀察哪個 state 的話,就會在任何 state 改變時都被觸發(因為 state 改變就會重新 render,重新 render 就會再次觸發一次 useEffect)

use-effect-flow

以剛剛 todo list 的例子來說,我的 component 中可能有 inputValuetodos 這兩個 state。

現在當我輸入一個字就會觸發一次 useEffect,因為我修改了 inputValue 的 state,而 state 變了就會重新 render,重新 render 就會再執行一次 useEffect,像下圖這樣:

use-effect-no-dependencies

但怎麼看都不合理嘛!應該要在 todo 這個 state 有改變時再做儲存的動作就好,而不是在任何 state 改變都去儲存。

因此這就是第二個參數的用途,讓你指定哪一個 state 改變時才執行這個 useEffect(這邊是為了方便解釋才這樣說。精確一點的說法是根據 dependencies 的內容是否有變來決定,有變的話才會觸發 useEffect

接著來改寫剛剛的範例:

1
2
3
4
5
useEffect(() => {
console.log('儲存 todos')
console.log('todos', todos)
window.localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]); // 告訴它我只想要 todos 改變時才執行這個 effect

use-effect-has-dependencies

加上這個參數後就能確保只有在 todos 改變時才去觸發 useEffect,避免了剛剛的問題。

所以人家常說的一次式 useEffect

只要你有弄懂剛剛的範例,就能明白為什麼下面這樣寫法只會執行一次:

1
2
3
useEffect(() => {
console.log('只會執行一次')
}, []);

因為 dependencies(陣列裡面)的內容永遠都是空的,所以不論接下來發生什麼事情它都不會改變,當然就不會再次觸發。

React 的第四個 hook:useLayoutEffect React 的第二個 hook:useRef
Your browser is out-of-date!

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

×