React Hooks (2) useEffect

useEffect 介紹

在 react 裡想取得遠端資料時,可以使用 useEffect 方法。這個 effect 指的是 副作用(side-effect) 的意思,在 React 中會把畫面渲染後和 React 本身無關而需要執行的動作稱做「副作用」,這些動作像是「發送 API 請求資料」、「手動更改 DOM 畫面」等等。

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
const App = () => {
console.log(1);
const [filterString, setFilterString] = useState("animal");

const onSearchHandler = (e) => {
setFilterString(e.target.value);
};

// (callback function, [])
useEffect(() => {
console.log(3);
const getPhotos = async () => {
const res = await axios.get(
`${api}?client_id=${accessKey}&query=${filterString}`
);
};
getPhotos();
}, []);

return (
<div>
{console.log(2)}
<SearchBox
onSearchHandler={onSearchHandler}
filterString={filterString}
/>
</div>
);
};

上面 console.log 執行順序為 1 => 2 => 3,因為 useEffect 內的 function 會在元件渲染完後被呼叫,這個時間點剛好非常適合來呼叫 API 並更新資料。

useEffect 生命週期

這邊介紹 useEffect 第二個參數會影響到的觸發時機

不加入任何條件時

當都不加入任何條件時 每次更新資料狀態都會觸發

1
2
3
useEffect(() => {
console.log(3);
});

只有加入一個空陣列

只加入一個空陣列時, 只有元件初始化(mounted)和元件移除(unmounted)時會執行

1
2
3
useEffect(() => {
console.log(3);
}, []);

加入 state 作為條件

當陣列內有加入 state 為條件時,只有元件初始化時 和 state 值有更動時會執行。

1
2
3
useEffect(() => {
console.log(3);
}, [filterString]);
  • Ref 的 current 屬性不可作為依賴項:
    • 更新 ref.current 並不會觸發重新渲染,所以 useEffect 並不會重新計算。
  • 可變值不可作為依賴項:
    • window 或 document 的屬性: ex: window.scrollY
    • React 元件外的變數,比如單純的全局變量(非 state),如 let isUserLoggedIn = true。

不要使用 Object 或 function 作為條件

React 在每次渲染元件時,如果將 Object 或 function 作為 Effect 的依賴,每次渲染時的 Object 或 function 都是不同的物件,就算物件裡內容都一樣。因此要盡量避免用 Object 或 function 作為條件。

  • 解法一: 將物件或函式移出元件

將物件 options 移出 ChatRoom 元件外層,options 就不再是響應式資料,所以不用加入依賴中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const options = {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};

function ChatRoom() {
const [message, setMessage] = useState('');

useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []);
}
  • 解法二: 將物件或函式移入 Effect 內:

物件移入 Effect 內後就不需傳入依賴中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
}
  • props 是物件的情況

當傳入元件的 props 是物件的情況,可以在元件內解構取得物件內的值後,在傳入 effect 使用

1
2
3
4
5
6
7
8
9
10
11
12
function ChatRoom({ options }) {
const [message, setMessage] = useState('');

const { roomId, serverUrl } = options; // 解構取得物件內的值
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);

使用限制

  1. useEffect 的限制,不可在 if 內使用,要放在元件最外層
1
2
3
4
5
6
// 無效,若需判斷請將 if 放在 useEffect 函式內
if (filterString === "animal") {
useEffect(() => {
console.log(3);
}, [filterString]);
}
  1. 本身不能作為 async function 使用
1
2
// 錯誤無法執行
useEffect(async () => {}, []);

要改為將 async 放在 useEffect 內

1
2
3
4
5
6
7
8
9
useEffect(() => {
const getPhotos = async () => {
const res = await axios.get(
`${api}?client_id=${accessKey}&query=${filterString}`
);
console.log(res);
};
getPhotos();
}, []);

移除事件監聽

當在 useEffect 裡加入事件監聽時,可以用 return 來移除監聽

1
2
3
4
5
6
7
// 當元件移除時,移除監聽
useEffect(() => {
window.addEventListener("resize", resizeHandler);
return () => {
window.removeEventListener("resize", resizeHandler);
};
}, []);

原理在於每次 useEffect 被執行時,return 的函式會優先被執行,來清除上一次的狀態

1
2
3
4
5
6
7
// 當元件移除時,移除監聽
useEffect(() => {
console.log("resourse change");
return () => {
console.log("return from resourse change");
};
}, [resourseType]);

如上每次 resourseType 更新時,都會優先執行 return from resourse change 在執行 resourse change

移除 api 請求

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
import { useState, useEffect } from "react";
import axios from "axios";

export function useFetch(url) {
const [loading, setLoading] = useState(true); // 加載狀態
const [data, setData] = useState(null); // 數據
const [error, setError] = useState(null); // 錯誤

useEffect(() => {
// 創建 Axios 的 CancelToken
const controller = new AbortController();

const fetchData = async () => {
setLoading(true); // 開始加載
try {
const response = await axios.get(url, { signal: controller.signal });
setData(response.data); // 成功時存儲數據
} catch (err) {
if (axios.isCancel(err)) {
console.log("Request canceled:", err.message);
} else {
setError(err); // 捕獲錯誤
}
} finally {
setLoading(false); // 結束加載
}
};

fetchData();
// 清理函數,用於取消請求
return () => controller.abort();
}, [url]);

return { loading, data, error };
}

和移除事件監聽一樣,也可以利用 return 的函式移除api連結。如上範例,使用 axios.get 發送 GET 請求,並傳遞 AbortController.signal 用來支持取消請求。讓組件卸載或 URL 改變時,中止請求以避免資源浪費或潛在問題。

初始化元件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let didInit = false;

function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
// ✅ Only runs once per app load
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
// ...
}

上面程式中,effect 雖然用了 [] 來確保只在元件初執行,但在開發模式中,一樣會被執行兩次。為了避免在開發模式中被執行兩次,在 App元件外宣告一個變數 didInit 來紀錄是否初始化過,effect 內檢查 didInit,若初始化過就不再初始化。

不要將不同的邏輯寫在同一個 effect

1
2
3
4
5
6
7
8
9
10
11
function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}

上面範例中,logVisit()connect(),是不相關的邏輯,應該要分成兩個 effect 撰寫,避免當 [] 內的依賴增加時,當不同的依賴更動時,也去執行到 logVisit。

1
2
3
4
5
6
7
8
9
10
11
function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
}, [roomId]);

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
// ...
}, [roomId]);
// ...
}