[ReactDoc] React Hooks - useEffect
本文章內容來自:
- Using the Effect Hook @ React Docs
- A Complete Guide to useEffect @ Overreacted
- You Might Not Need an Effect @ React Docs Beta
useEffect
的使用時機類似在傳統 class 中使用 componentDidMount
, componentDidUpdate
和 componentWillUnmount
的生命週期,關於 useEffect 的基本說明可以參考「React Hooks 起步走」。
在 Dan Abramov 的 A Complete Guide to useEffect 中提到,在學習 useEffect
時,我們應該忘記過去對於 React 生命週期所學到的,以全新的體驗和思考方式來認識 useEffect
。
筆記
- 使用
useEffect
一定要留意第一次執行的時間點,如果第一次不想要執行要 return 掉 - 使用
useEffect
一定要留意 dependencies 陣列中的變數,它們的改變會觸使 useEffect 執行 - dependencies array 是透過 Object.is() 來比較兩個值是否相等
// 第一次資料進來時 state 可能是 undefined(例如 AJAX),此時不要執行
useEffect(() => {
if (hasScreenPermission === undefined) return;
// 第二次拿到 state 後才執行...
setState({
error: 'foobar',
});
}, [hasScreenPermission, setState]);
留意重點
- 簡單來說,
useEffect
內的 effect 會在每一次 DOM 轉譯後被呼叫( 不論是 mounted 或 updated);如果有回傳 cleanup function 的話,則會在每一次 DOM 轉譯後的最開始先被呼叫,接著才執行該次的 effect(元件 unmount 或每次 updated 前被呼叫)。 - 實際上在每一次元件轉譯時
useEffect
內的函式都是新的、不同的,並會把舊的給覆蓋;換句話說,effect 會更像是隸屬於每次轉譯的一部分。 - 由於這些 effects 會在每次元件轉譯時都執行(而非只執行一次),因此實際上
useEffect
中回傳的 cleanup 函式並非只有在元件 unmount 時被呼叫到,而是在每次執行該 effect 前也會呼叫一次 clean up 的函式。 - 和
useState
一樣,在一個元件中可以多次使用useEffect
,因此你可以根據程式邏輯拆成許多不同的 effects。
Hooks 基本使用
在 React 元件中有兩種常見的副作用(side effects),一種需要被清理(cleanup),一種則不用。
不需要被清理的作用(Effects Without Cleanup)
有時我們會需要在 React 更新完 DOM 之後來執行其他的程式碼,像是網路請求(Network request)、手動操作 DOM(manual DOM mutation)或紀錄(logging),這些都是常見不需要被清理的作用,也就是做完就不用再去管它。
透過使用 useEffect
這個 Hook,你將可以在元件轉譯完成後去做一些事,React 會記得你傳入 useEffect
內的 function(這個 function 我們稱做 effect),並在執行完 DOM 的更新後呼叫它。
預設的情況下這些 effect 會在元件第一次被轉譯以及後續每一次更新時被呼叫到(除非去自訂這樣的行為),因此,現在不必再去想這個方法需要在 mounting 還是 updating 時被呼叫,只要知道這些 effect 會在元件「轉譯後(after render)」被呼叫,React 會確保 DOM 已經更新完成後才去呼叫這個 effect。
和 componentDidMount
或 componentDidUpdate
不同的地方在於,在 useEffect
中的 effects 並不會使得瀏覽器阻塞而無法更新畫面,大部分的 effects 並不需要同步(synchronously)處理,少部分除了像是要測量 layout 這種 effect,則會使用 useLayoutEffect
Hook。
範例程式碼
// https://reactjs.org/docs/hooks-effect.html#example-using-hooks
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
需要被清理的作用(Effects with Cleanup)
有些時候我們需要訂閱(subscription)一些外部資料,在這情況下「清理(cleanup)」就顯得很重要,如此才可以避免記憶體洩漏(memory leak)的問題。
在傳統 React class 的寫法中,我們會在 componentDidMount
時去註冊監聽某事件,在 componentWillUnmount
時去移除監聽事件的註冊。
在 useEffect
中,則可以回傳一個清理用的函式(可以是匿名函式),它會在 React 元件要 unmount 時被執行,此外,由於每次元件轉譯時,執行的都是一個全新的 effect,因此在每此元件轉譯前也都會執行 cleanup 的函式。也就是說,cleanup 函式會在「每次元件轉譯前」,以及「元件 unmount 」時被執行。
Explanation: Why Effects Run on Each Update @ React Docs
範例程式碼
// https://reactjs.org/docs/hooks-effect.html#example-using-hooks-1
import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
// 事件處理器(event handler)
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
// 註冊事件
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// 定義清理的方法並回傳
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
非同步請求資料時(async to fetch data)
要特別注意的是,useEffect 的回傳值只能是空或者是清理函式(cleanup function),因此 callback 並不能帶入 async function,因為 async function 實際上是回傳一個 implicit promise。所以如果需要使用非同步存取資料時,需要將 async function 獨立出來放在 useEffect 內,或進一步參考後面的[ 式](#定義在 function component 內使用的函式):
// Effect callbacks are synchronous to prevent race conditions.
// Put the async function inside:
useEffect(() => {
async function fetchData() {
// You can await here
const response = await MyAPI.getData(someId);
// ...
}
fetchData();
}, [someId]); // Or [] if effect doesn't
範例程式碼
/**
* https://www.robinwieruch.de/react-hooks-fetch-data
* fetch data with constant
* fetch data with variable
* fetch data when use click button
* fetch data with loading indicator
* fetch data with error handling
* see diff on gist
* https://gist.github.com/PJCHENder/54108cffe99d6e94f99a8e3577ed3d3e/revisions
*/
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function DemoOfFetchData() {
const [data, setData] = useState([]);
const [query, setQuery] = useState('');
const [queryString, setQueryString] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(`https://hn.algolia.com/api/v1/search?query=${queryString}`);
setData(result.data && result.data.hits.filter((item) => item.title));
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [queryString]);
return (
<div style={{ padding: 60 }}>
<input type="text" value={query} onChange={(e) => setQuery(e.target.value)} />
<button type="button" onClick={() => setQueryString(`query=${query}`)}>
Search
</button>
{isError && <div>Something went wrong...</div>}
{isLoading ? (
<div>Loading ...</div>
) : (
<ul>
{data.map((item) => (
<li key={item.objectID}>
<a href={item.url}>{item.title}</a>
</li>
))}
</ul>
)}
</div>
);
}
export default DemoOfFetchData;
使用相依陣列(dependency array):略過某些 effect 以提升效能
keywords: 相依陣列
, dependency array
, deps
- Is it safe to omit functions from the list of dependencies? @ React Docs - Hooks FAQ
- Conditionally firing an effect @ React Docs - Hook API Reference
- Teaching React to Diff Your Effects @ Overreacted
在過去 React class 中,會在 componentDidUpdate
內使用一些判斷式避免在不必要的元件更新,例如說只有在 prevState
和當前 this.state
的狀態不同時,才會更新:
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}
帶入相依陣列(dependency array):effect 只會在陣列內容變更時被執行
在 useEffect
中可以做到類似的效果,當某些值沒有改變的時候,你可以叫 React 在重新轉譯時(re-renders / update)跳過某些 effect,只需要在 useEffect
的第二個參數中帶入「相依陣列(dependency array, deps)」即可,只要陣列中的元素沒有改變時,React 就會跳過該 effect,但只要陣列中有任何一個值不同,就不會跳過 effect:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 只會在 count 有改變時重新執行該 effect
⚠️ Best Practice: 最好的方式就是把所有在
useEffect
內有使用到的state
或props
都定義在相依陣列中。
帶入空陣列:將相依陣列帶入空陣列(小心使用!)
如果該 effect 內沒有使用到任何 states 或 props,而你希望這個 effect 和 cleanup 只會被執行一次(只有在 mount 和 unmount),那個第二個參數可以帶入一個空陣列([]
),這會告訴 React 這個效果不會依賴到任何 props 或 state,因此它完全不需要重新執行。
❗ 只有在該
useEffect
內沒有使用到的任何 props 或 states 的情況下可以帶入空陣列[]
。
// 若在 effect 內沒有使用到任何相依的 states 或 props
// 可以在 `useEffect` 的第二個參數帶入 []
// 如此 updated 時都不會被呼叫到
useEffect(() => {
// 只會在 mounted 時執行一次 effect
console.log('--- useEffect: no dependent on states or props ---');
return () => {
// 只會在 unmount 時執行一次 cleanup
console.log('--- cleanup useEffect: no dependent on states or props ---');
};
}, []);
若沒有在第二個參數中帶入 []
,則該函式會每次重新轉譯時(updated
)都呼叫到。
💡 提示:當相依陣列裡放入的是
dispatch
,setState
,useRef
可以省略,因為 React 會確保它們是靜態的。