請解釋 useEffect?與 useLayoutEffect 的區別?
2023年2月2日
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 已經更新為新的隨機數字,這時畫面才進行重繪。
延伸問題
如果不指定 dependencies ,useEffect
什麼時候會執行?
此 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。