請解釋 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