useEffect
是 React 中常用的一個 Hook,也是前端面試中經常被問到的 React 面試題,包括如何使用 useEffect
、useEffect 的執行時機等。另一個與 useEffect
相似的 Hook 是 useLayoutEffect
,這兩者的比較也是 React 面試的高頻題。
useEffect
是什麼?
useEffect
是一個用於連接外部系統的 React Hook。React 的函式元件需要是純函式,但如果我們需要執行具有副作用 (side effect) 的操作,例如:請求 API、使用第三方函式庫,我們就需要將這些程式碼放在 useEffect
中執行。外部系統例如是:伺服器端、瀏覽器提供的 API 或是第三方函式庫。因為這部分不是由 React 本身處理的,所以稱為外部系統。
useEffect
使用方法規則
只能在頂部呼叫
useEffect
也是一種 hook,因此只能在頂層呼叫,不能在迴圈、條件式或者巢狀的 function 中使用,想了解細節的朋友,可以前往這篇文章《為什麼只能在最頂端層呼叫 Hook?從 useState
實作原理來回答》
useEffect
接受兩個參數:setup function 和 dependencies(可選)
-
setup function
setup function:setup function 包含如何連結外部系統的程式碼,如果需要清除邏輯,可以在 setup function 中回傳一個清除 function。
-
dependencies dependencies 參數是可選的陣列,可以傳入 props、state 或元件中任何使用的變數。React 會使用
Object.is
算法來進行比較, (想了解Object.is
的細節可以閱讀此篇文章《在 JavaScript 當中,==、=== 與Object.is
()的區別》)。如果 dependencies 中任意一個值與前一次不同,則此useEffect
會重新執行。。
import { useEffect } from 'react';
import { createConnection } from './blog.js';
function Article({ articleId }) {
const [serverUrl, setServerUrl] = useState('https://blog.com/0');
useEffect(() => {
const connection = createConnection(serverUrl, articleId);
connection.connect();
// 回傳 cleanip function
return () => {
connection.disconnect();
};
}, [serverUrl, articleId]);
// ...
}
useEffect
執行時機
- 當元件被加入時 (mount),
useEffect
會被第一次執行。 - 當每次元件重新渲染時,如果 dependencies 的值有改變,先將舊的 props 和 state 執行 cleanup function,再帶著新的 props 和 state 執行 setup function。
- cleanup function 的程式碼,會在元件生命週期結束 (unmount) 時,執行最後一次。
useEffect
,畫面重新渲染時都會閃爍要怎麼解?
使用 解法:嘗試將 useEffect
換成使用 useLayoutEffect
。
以下這一段我們會討論跟 useEffect
很相近的 Hook - useLayoutEffect
。
而標題已經點出來,useLayoutEffect
通常會拿來處理當使用 useEffect
但畫面會出現閃爍的情境,詳細原因下面會提到。
useLayoutEffect
是什麼?
useLayoutEffect
其實是 useEffect
的一種版本,傳入的參數也一樣,只是執行時機不一樣,它會在瀏覽器重繪 (repaints) 前執行。
useLayoutEffect
可能會造成性能的問題,因為在 useLayoutEffect
裡的程式碼會阻礙瀏覽器重繪 (repaints) ,太頻繁使用可能會造成整個應用程式緩慢。因此通常會建議先使用 useEffect
,如果不能解決問題,才會選擇使用 useLayoutEffect
。
useEffect
和 useLayoutEffect
比較
以下提供一個程式碼範例,可以明顯感到這兩者差別。 (備註:以下程式碼範例是為了凸顯這兩者差別,實際上開發並不會這樣寫)
程式碼
import { useEffect, useLayoutEffect, useState } from 'react';
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
if (count === 0) {
const randomNum = 1 + Math.random() * 1000;
setCount(randomNum);
}
}, [count]);
return <div onClick={() => setCount(0)}>{count}</div>;
}
當我們執行上方程式碼時,連續點擊 div 區塊,會看到畫面產生閃爍。
原因是,當你每次點擊 div,此時 count 會被更新為 0,畫面會重新渲染變為 0,同時,因為 count 被更新,也會觸發 useEffect
執行。所以在重繪完成之後, useEffect
執行並把 count 更新為另一串隨機數字,畫面也會再渲染一次,因為兩次渲染時間很快,所以造成閃爍。
useLayoutEffect
的差異是什麼 ?
那 假設我們把上方程式碼的 useEffect
換成 useLayoutEffect
,當你每次點擊 div,此時 count 會被更新為 0,但這時,畫面不會被重新渲染變為 0,而是先等待 useLayoutEffect
內的程式碼執行完畢之後,state 已經更新為新的隨機數字,這時畫面才進行重繪。
延伸問題
useEffect
什麼時候會執行?
如果不指定 dependencies ,此 effect 會是在每次重新渲染元件後重新執行
如果 dependencies 中有 object 會怎麼樣?
下方程式碼的 options 物件會在元件每次重新渲染時,都是不同的物件,所以這個 useEffect
可能會在每次渲染時都重新執行,因為在 dependencies 中的 options 有改變。
function ChatRoom({ articleId }) {
const [article, setArticle] = useState(null);
// options 會在每次渲染時重新創建
const options = {
serverUrl: 'https://localhost:1234',
articleId: articleId
};
useEffect(() => {
const data = getArticle(options);
setArticle(data)
// options 每次渲染時值都不同,因此觸發 useEffect 執行
}, [options]);
如果要避免上方程式碼造成不必要觸發 useEffect
,其實可以把動態的物件放在 Effect 中,並把 dependencies 中的物件改為 string 或 number,如下。
function ChatRoom({ articleId }) {
const [article, setArticle] = useState(null);
useEffect(() => {
const options = {
serverUrl: 'https://localhost:1234',
articleId: articleId
};
const data = getArticle(options);
setArticle(data)
}, [articleId]);
useLayoutEffect
?
什麼時候使用 如上方說到,useLayoutEffect
頻繁使用會損害應用程式的效能,只有在一些特別情況才建議使用。以下提供一個使用情境 (此例子來自於 React Docs Beta)。
假設今天我們要設計一個工具提示元件 (tooltip),它會依照不同條件出現在某元素的上方、下方或旁邊,這種設計條件代表說,我們會需要知道此元素準確的高度位置,才能判斷要將 tooltip 顯示在哪裡。
React 的渲染步驟可以拆分為下
- 在任何地方渲染 Tooltip (即使位置不正確)
- 測量元素的高度,並決定放置 Tooltip 的位置
- 重新渲染畫面,這時 Tooltip 的位置才會是正確的
如果是使用 useEffect
的話,Tooltip 的位置,可能是從 0 變到 10 的位置,這會造成畫面閃爍、使用者體驗不佳,如果是使用 useLayoutEffect
,React 則會在重繪前,就重新計算正確的位置,才渲染畫面。
程式碼可參考此連結 React Docs Beta。