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); };
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 內後就不需傳入依賴中。
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 是物件的情況,可以在元件內解構取得物件內的值後,在傳入 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]);
|
使用限制
- useEffect 的限制,不可在 if 內使用,要放在元件最外層
1 2 3 4 5 6
| if (filterString === "animal") { useEffect(() => { console.log(3); }, [filterString]); }
|
- 本身不能作為 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(() => { 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; 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]); }
|