即時精選

探索 React Hook —— useEffect:掌握副作用的用法、陷阱與替代方案

zxlee-avatar-img
發佈於React
更新 發佈閱讀 43 分鐘

剛開始學習 React 的時候,我覺得 useEffect 就像「魔法一樣」,從資料獲取與更新、追蹤使用者行為,到訂閱連線⋯⋯,這些讓畫面更有活力的功能背後,幾乎都與 useEffect 有關。

不過,useEffect 固然強大萬能,卻又時常令人摸不著頭緒,究竟它司職什麼?在元件的生命週期中扮演怎樣的角色?這篇文章會從它的定義開始,一步步探索它的常見用法、容易踩雷的地方,以及為什麼 React 官方文件會說:「你也許不需要 useEffect」。

useEffect 做了什麼?

在沒有 useEffect 的世界中,React 的函式元件主要只負責一件事:

根據狀態與 props,計算並繪製出呈現在使用者眼前的介面。

這是 React 的設計原則:元件的函式應該要是「純的」(pure function),也就是每次執行,都會渲染出同樣的畫面結果。但實務上的需求,往往更加複雜,時常需要與「外部系統」(external system) 互動,比如說:

  • 瀏覽器 API(DOM、windowdocument⋯⋯)
  • 網路(fetch、WebSocket⋯⋯)
  • 第三方套件(地圖、播放器、圖表庫⋯⋯)
  • 訂閱型系統(Timer、Event Emitter、Observer⋯⋯)

我們會將這些互動產生的行為稱為「副作用」。外部系統在 React 掌控範圍之外,因此,我們需要在元件渲染完成後,很明確地告訴 React,是否要與外部系統連線、什麼時候要斷開,而 useEffect 就是在其中扮演同步的角色。

簡單來說,我們可以簡單地用這句話來理解 useEffect

當畫面更新完成後,如果條件成立,就去做這些事。

如何撰寫 useEffect?

以下是 useEffect 的基本語法:

useEffect(effectFn, dependencies?)

useEffect 本身不回傳值,它接受兩個參數:

  1. effectFn:執行副作用邏輯的回調函式,包含兩個部分——
    a. 每次觸發時運行的副作用邏輯,至少會在元件掛載時運行一次
    b. 可選的清理函式 (cleanup),如需使用,放在 return 後方作為 effectFn 回傳的函式,它會在元件卸載,以及每次副作用重新執行前觸發。
  2. dependencies:可選的依賴項,決定副作用觸發的時機,它有三種情況——
    a. 不傳入:副作用每次渲染時都會執行,但實務上很少這麼做
    b. 空陣列 []:只在元件掛載時執行
    c. 有值,即 [dep1, dep2, ...] 的狀況:通常是狀態或 props,只要陣列中任一值改變,就會執行清理函式(如果有的話),接著重新執行副作用。

值得注意的是,雖然我們擁有將哪些值當作依賴項的最終決定權,但如果副作用邏輯(effectFn)包含狀態、props 等會觸發重新渲染的值的話,React 官方建議務必將它們放入依賴陣列中。

最後,讓我們將 useEffect 展開,基本上會看到這樣的結構:

useEffect(() => {
// 主要的副作用邏輯

return () => {
// 清理函式邏輯(可選)
}
}, [dep1, dep2, ...]) // 依賴項

React 元件的生命週期

如果你對上述那些掛載、卸載、渲染等名詞感到陌生,你並不孤單,我也是這樣,就算是這樣,依然還是可以寫出能運作的元件。不過話雖如此,理解執行順序,我認為還是十分重要的。

想像元件的生命週期分成三個階段:

  1. 掛載階段 (mount) ——元件首次被加到 DOM 上的過程。
    執行元件函式 → 根據 JSX 建立虛擬 DOM →
    加到真實 DOM → 畫面完成繪製 → 執行 useEffect。
  2. 更新階段 (update) ——當 state、props 或 context 改變時,元件重新渲染的過程。
    執行元件函式 → 建立新的虛擬 DOM → 比對新舊虛擬 DOM 差異 →
    提交更新到真實 DOM → 畫面完成繪製 →
    若 useEffect 依賴項有變動,先執行清理函式,再重新執行。
  3. 卸載階段 (unmount) —— 元件從 DOM 上被移除的過程。
    準備移除元件 → 執行 useEffect 的清理函式 → 移除元件 → 釋放記憶體。

決定元件掛載/卸載的因素,通常有以下幾種:

  • 條件渲染
  • 路由切換
  • key 值更新
useEffect 必定會在掛載時,執行一次副作用;如果有回傳清理函式,也必定在卸載前執行。

接著,讓我們用流程圖來看看元件的運作流程:

元件的運作流程

元件的運作流程


useLayoutEffect:同步版本的 useEffect

useEffect 不會在渲染過程中執行,而是在瀏覽器完成繪製畫面之後才會開始動作,這意味使用者已經看到更新後的畫面,你的副作用才會開始運作,因此,副作用是能夠讀取到已經更新好的 DOM。

但是,也因為如此,如果你的副作用是為了某種視覺效果,而且會因為 useEffect 的運作而有所延遲,或出現畫面閃爍。如果不希望使用者看到這樣的中間狀態,可以改用 useLayoutEffect

useLayoutEffect會在畫面繪製前執行,可以確保介面呈現出來時,副作用已經開始執行,但也因為這樣,可能會造成畫面的阻塞,因此除非是真的需要與 DOM 同步或是修改 DOM,否則還是優先考慮讓 useEffect 執行副作用。

如果你依然對副作用的執行流程感到混亂,我在 CodePen 寫了一個計數器範例,點擊加減按鈕會導致狀態更新,讓元件重新渲染;而點擊 Remount 則會讓元件卸載後重新掛載,你可以點擊進去,打開 console,觀察副作用與重新渲染、重新掛載前後的觸發順序。

常見用法

一、元件掛載時抓取 API 資料

這應該是 useEffect 最典型的用法:當元件掛載時,從特定 API 抓取資料,並顯示在 UI 上。

import { useState, useEffect } from "react";

const apiPath = "https://jsonplaceholder.typicode.com/posts?_limit=5"

function PostList() {
const [posts, setPosts] = useState([]);
const [isLoading, setIsLoading] = useState(false);

useEffect(() => {
let isCanceled = false;

const fetchPosts = async () => {
setIsLoading(true);

try {
const res = await fetch(apiPath);
const data = await res.json();
if (!isCanceled) setPosts(data);
} catch(e) {
console.error(e);
} finally {
if (!isCanceled) setIsLoading(false);
}
}

fetchPosts();

return () => {
isCanceled = true;
}
}, []); // 只在元件掛載時抓取資料

if(isLoading) return <p>Loading...</p>

return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
};

在這個例子裡,useEffect 在做的是,與網路/伺服器(外部系統)同步資料。倘若需要設定重新抓取資料的條件,可以將指定的狀態或 props 放入依賴項之中。

注:isCanceled 是為了防止「元件卸載之後,但非同步請求還回來嘗試更新狀態(setPosts)的狀況,實務上,可能會更常看到使用 AbortController API 來中斷請求(詳見:MDN

不過儘管使用 useEffect 抓取資料,應該算是這個 Hook 最典型的用法,我首次接觸 useEffect 也是用在這裡。但是,React 官方文件也點出了這個做法的幾個問題,包括:

  • 副作用並不在伺服器執行
    意味著客戶端必須載入所有的 JS bundle 才能抓取資料
  • 容易導致網路請求的「瀑布效應」(network waterfalls)
    即父元件載完自己的資料才會載入子元件的資料
  • 通常意味著沒有預載 (preload) 以及快取 (cache)
  • 反覆且冗長的程式碼

因此,抓取資料的方式,官方文件反而建議使用 TanStack Query、useSWR 或是 React Router 6.4+ 等套件來達成,或是使用框架(如 Next.js)內建的抓取資料機制。當然,你也可以撰寫客製化 Hook 來解決以上問題。

二、訂閱服務

有一種外部系統提供的服務叫做「訂閱」(subcribe),看起來很抽象,不過它運作起來,就像是訂閱 YouTube 頻道或報章雜誌一樣,我們告訴服務方「每當頻道更新,讓我知道」、「每隔一段時間,把刊物送到我手中」——而監聽計時,就是程式設計最具代表性的訂閱系統。

在 React,是透過 useEffect 來達成與外部系統的訂閱,但值得注意的是,訂閱型服務通常不會隨元件卸載而消失,這也是為什麼當副作用作為這種用途時,清理函式往往是不可或缺的:因為如果沒有「取消訂閱」,在使用者來回切換頁面的同時,這些服務會繼續默默在背景執行,佔據瀏覽器資源。

現在,讓我們看看範例:

A、監聽事件addEventListener vs. removeEventListener

監聽事件,可以說是最常見的訂閱範例,如果想要追蹤視窗寬度、捲軸滾動進度、鍵盤事件等資訊時,就可以透過訂閱事件監聽器達成:

import { useState, useEffect } from "react";

function WindowWidth() {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);

useEffect(() => {
const handleResize = () => {
setWindowWidth(window.innerWidth);
};

window.addEventListener("resize", handleResize);
console.log('Event "resize" has been subscribed');

return () => {
window.removeEventListener("resize", handleResize);
console.log('Event "resize" has been cleared');
};
}, []);

return <p>Window Width: {windowWidth}</p>;
}

B、計時器訂閱setInterval vs. clearInterval

當你希望每隔一段時間,執行某個動作時,可以藉由 setInterval 來訂閱計時器的服務:

import { useState, useEffect } from "react";

function CountdownTimer() {
const [seconds, setSeconds] = useState(60);
const [isActive, setIsActive] = useState(false);

useEffect(() => {
let intervalId = null; // --- 1.

if (isActive && seconds > 0) {
intervalId = setInterval(() => {
setSeconds((s) => s - 1); // --- 2.
}, 1000);
}

console.log("Start countdown...");

return () => {
if (intervalId) {
clearInterval(intervalId);

console.log("Timer has been cleared");
}
};
}, [isActive]);

return (
<div>
<h2>Countdown Timer</h2> 
<p}>{seconds} left</p> 
<button onClick={() => setIsActive(!isActive)}>
 {isActive ? "Pause" : "Start"} 
</button>
 <button onClick={() => setSeconds(60)}>Reset</button>
</div>
);
}
  1. setInterval 會回傳一個識別碼,而這個識別碼正是需要在 clearInterval 的參數傳入的值,因此將它存在自定義的變數 intervalId 中,作為清理函數的 clearInterval 依據。
  2. 如果是撰寫 setSeconds(seconds - 1),你的副作用捕捉到的將會是「當次渲染的 seconds 狀態」,因此在畫面看到的,將會停留在 59,不會繼續倒數;
    如果再將 seconds 放在依賴陣列中(Eslint 也會建議你這麼做),雖是可行的方案,但每次 seconds 改變觸發重新渲染,都需要處理 interval 的清理與重建,顯然不是好的實踐方式。
    因此,在這裡透過 useState 的 updater 來更新狀態, 既能避免將 seconds 放入依賴項,也能確保讀取到的會是 seconds 的最新值。

三、整合第三方函式庫的工具

許多第三方函式庫提供的工具通常有自己的生命週期,其中像是與 UI 相關的工具,通常需要取得 DOM 節點才能初始化,讓我們先複習 React 函式元件的渲染流程:

執行副作用之外的函式邏輯
→ 渲染 JSX
→ 顯示 DOM 畫面
→ 執行副作用

因此,要抓取特定 DOM 元素,必須等待元件渲染完成,也就是需要放在副作用的邏輯之中。要達成這點,通常搭配 useRef 來取得該元素的參考,待真實 DOM 繪製完成後,再透過這個參考,初始化 UI 介面。以下用原生 Swiper 套件來示範,實務上你也可以使用官方已經封裝好的 React 元件版本。

要用原生的方法取得 Swiper 實例,我們需要傳入 DOM 元素作為它的容器,也就是:

new Swiper(domContainer, options?)

如上所述,我們要在元件完成渲染後,才能將實例初始化,因此,第一步將是先透過 useRef 來取得指定 DOM 元素的參考,我們將其命名為 swiperRef

import { useEffect, useRef } from "react";
import Swiper from "swiper";

function ImageCarousel({ images }) {
const swiperRef = useRef(null);

useEffect(() => {
// 初始化 Swiper 實例
}, []);

return (
<div className="swiper" ref={swiperRef}>
<div className="swiper-wrapper">
{images.map((src) => (
<div className="swiper-slide" key={src}>
<img src={src} />
</div>
))}
</div>
</div>
);
}

swiperRef 拿到 DOM 元素,執行 useEffect 來初始化 Swiper 實例:

useEffect(() => {
if (!swiperRef.current) return;

const swiperInstance =
new Swiper(swiper.current, { // 👈 DOM element as the swiper container
// options like slidePerView, spaceBetween...
});
}, []); 

但是,很多第三方工具會建立 DOM 節點或是監聽視窗事件,Swiper 就是其中之一。因此當元件卸載時,需要靠清理函式來釋放所佔據的資源,以避免記憶體洩露。為此,我們需要用到在 Swiper 實例中,所提供的 destroy 方法。

swiper.destroy(shouldDeleteInstance = true, shouldCleanStyles = true);

加上清理函式:

useEffect(() => {
if (!swiperRef.current) return;

const swiperInstance = new Swiper(swiper.current, {
// options...
});

// 👇 清理函式
return () => {
swiperInstance.destroy(true, true);
};
}, []);
💡 配合動態資料來更新副作用:

目前傳入 useEffect 的依賴項是空陣列,意味著副作用只會在元件掛載時執行。如果你的副作用需要根據動態資料來更新,固然可以把動態的值直接放入依賴項之中,但這會導致整個實例重建。

useEffect(() => {
if (!swiperRef.current) return;

const swiperInstance = new Swiper(swiper.current, {
// options...
});

return () => {
swiperInstance.destroy(true, true);
};
}, [dynamicData]); // 👈 每次 dynamicData 更新,重建實例

許多時候重建實例是不需要且所需成本較高的,因此這個時候更建議將邏輯分開。

以 Swiper 的例子來說,我們會用到套件所提供的 update 方法,因此,透過 useRef 的參考取得實例後,便能在元件中使用實例中的方法:

// 建立一個給 swiper 實例用的參考
const swiperInstanceRef = useRef(null);

useEffect(() => {
if (swiperRef.current){
// 改用參考取得 swiper 實例
swiperInstanceRef.current =
new Swiper(swiperRef.current, options);
};

return () => {
swiperInstanceRef.current.destroy();
};
}, []);

useEffect(() => {
if (swiperInstanceRef.current){
swiperInstanceRef.current.update()
}
}, [dynamicData]); // 將職責獨立出來,讓依賴項只影響依賴它的副作用

為什麼「你也許不需要 useEffect」?

在 React 的官方文件,特別將 useEffect 定義為與外部系統同步的工具,這意味著,如果當你使用 useEffect 時,不是在處理外部系統的同步,很可能就是你根本不需要使用 useEffect。以下為常見案例:

一、衍生狀態的計算

當希望根據狀態或 props 變化,來計算一個新的狀態時,一時間可能會想要透過 useEffect 來處理:

function FullNameDisplay() {
const [firstName, setFirstName] = useState("John");
const [lastName, setLastName] = useState("Doe");

const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`)
}, [firstName, lastName]);

// ...
};

不過,這樣的寫法不僅讓 React 多追蹤一個不必要的狀態(fullName),且會在 firstNamelastName 改變後,在處理副作用時,因為 fullName 狀態的改變,立即多觸發一次重新渲染。但其實,這個 fullName 是可以用變數宣告的方式定義,在渲染的過程中計算出來的:

function FullNameDisplay() {
const [firstName, setFirstName] = useState("John");
const [lastName, setLastName] = useState("Doe");

const fullName = `${firstName} ${lastName}`;

// ...
};

另外一個類似的例子,用 useEffect 計算 props 的衍生值——

function FilteredList({ items, filterText }) {
const [filteredItems, setFilteredItems] = useState([]);

useEffect(() => {
setFilteredItems(
items.filter(i => i.name.includes(filterText))
);
}, [items, filterText]);

//...
};

可以改成直接在渲染中計算,省去不必要的 state 跟 effect:

function FilteredList({ items, filterText }){
const filteredItems = items.filter(i =>
i.name.includes(filterText)
);

//...
};

如果這個 filter 的計算是昂貴的,可以使用 useMemo 來做快取:

const filteredItems = useMemo(() => {
// other calculations...

return items.filter(i =>
i.name.includes(filterText)
);
}, [items, filterText]); // items 或 filterText 改變才重新執行

二、元件狀態的重設

如果希望元件根據狀態或 props 的改變,而重設整個元件的狀態,你可能會希望透過 useEffect 來達成,比如說有一個表單,我們希望當用戶 ID 改變時重置表單裡的值:

function Form({ userId }){
const [value, setValue] = useState("");

useEffect(() => {
setValue("");
}, [userId]);

// ...
};

不過,通過 props 在副作用改變狀態容易讓資料流變得複雜,且由於副作用的執行在元件渲染之後,這樣撰寫會先將原有的狀態渲染出來,接著才清空表單,這也許不是你希望的效果。

這種時候基本上可以將不同 userId 的 Form 視為不一樣的元件,因此官方文件建議透過 key 值讓整個子元件重新掛載:

function FatherComponent(){
return <form key={userId} userId={userId} />
};

function Form({ userId }){
// 此元件的狀態會因為 key 改變而自動重置
const [value, setValue] = useState("");

//...
};

三、處理事件邏輯

有時候,你可能會希望在 useEffect 處理事件邏輯,尤其是當邏輯重複時。比如說,在商品頁面,我們希望在點擊「加入購物車」按鈕或「結帳」按鈕時,呈現通知訊息。為了避免重複,我們也許會希望在 useEffect 中來處理通知訊息的邏輯:

function ProductPage({ product, addToCart }) {
// 在副作用處理事件相關邏輯
useEffect(() => {
if(product.isInCart){
showNotification(`${product.name} 已放入購物車`);
}
}, [product]);

function handleBuyClick() {
addToCart(product);
};

function handleCheckoutClick() {
addToCart(product);
navigateTo("/checkout");
};

// ...
};

但這其實是容易出現不預期狀況的寫法,如果你的元件會記住購物車的狀態,那麼當加入購物車、頁面重新載入時,showNotification 會再次執行,因為 isInCart 已經是 true。

要改寫它,可以把共同邏輯給包進函式裡,然後在需要它的事件處理器中呼叫:

function ProductPage({ product, addToCart }){
// 用函式將共同邏輯抽出,再放入事件處理器當中
function buyProduct() {
addToCart(product);
showNotification(`${product.name} 已放入購物車`);
};

function handleBuyClick() {
buyProduct();
};

function handleCheckoutClick() {
buyProduct();
navigateTo("/checkout");
};

// ...
};

總結來說,「你可能不需要 useEffect」的狀況主要可以往這些方向思考:

  1. 是否在副作用中觸發了重新渲染(setState)?
  2. 是否是在副作用處理了衍生狀態的計算?
  3. 資料的流動是否過於複雜?

如果這些操作能在渲染的過程中完成,它可能就不需要 useEffect

避免直接在 useEffect 的依賴項中放入不必要的物件資料

React 透過 Object.is() 來比較 useEffect 的依賴是否改變,如果改變了,就重新執行副作用。

不過對 JavaScript 的物件資料(物件、陣列、函式⋯⋯)來說,每次元件函式因重新渲染的再次執行,都會為物件資料創建新的參考,這導致即使這個資料看起來相同,但 Object.is() 會認為它們已經不一樣,造成不預期的副作用重新執行。比如說:

function MyComponent() {
const [rerender, setRerender] = useState(false);
const handleRerender = () => setRerender((r) => !r);

const obj = {};

const fn = () => {};

useEffect(() => {
alert("The effect on obj is triggered");
}, [obj]);

useEffect(() => {
alert("The effect on fn is triggered");
}, [fn]);

return <button onClick={handleRerender}>Re-render</button>;
}

當每次點擊按鈕,即使這兩個副作用都沒有依賴 rerender 這個狀態,但依然會在重新渲染時分別觸發。Demo:

要解決這種狀況,可以直接將這些依賴放在副作用定義,避免將其放入依賴,進而減少依賴陣列的長度;若需要在他處使用,可以分別使用 useMemouseCallback 來穩定物件及函式的參考。

用 useMemo 穩定物件參考

function MyComponent({ category, status }) {
const filters = useMemo(() => ({
category,
status,
limit: 10
}), [category, status]); // 只有 category 或 status 改變時才創建新物件

useEffect(() => {
fetchData(filters);
}, [filters]);

// ...
}

除此之外,可以思考是否真的需要使用整個物件作為依賴,如果只是想要依賴物件中的某些原始值,就能把它抽離出來放進依賴:

function MyComponent() {
const [filters, setFilters] = useState({ category: "tech", status: "active" });

useEffect(() => {
fetchData(filters);
}, [filters.category]); // 只依賴原始值

// ...
}

用 useCallback 穩定函式參考:

function MyComponent({ userId }) {
const fetchUser = useCallback(() => {
fetch(`/api/users/${userId}`);
}, [userId]); // 只有 userId 改變時才創建新函式

useEffect(() => {
fetchUser();
}, [fetchUser]);

// ...
};

新 Hook:useEffectEvent 與 useEffect 的關係

隨著 React 19.2 版本到來,React 正式引入了 useEffectEvent ,這個新 Hook 讓你得以從副作用中,提取出一個不應該觸發副作用的邏輯,到一個可重用的函式中。

也就是說,useEffectEvent 讓我們可以定義一個函式,在 useEffect 中不需要將它放入依賴項中,但仍能夠讀取到像是狀態、props 等最新的值。如此一來,這個 useEffect 就不會因為這些值的改變,而觸發副作用的執行。

useEffectEvent 解決了長期以來困擾 React 開發者的痛點,因為有時候,我們希望在副作用使用某些東西,而這些東西,我們又不想要放入依賴陣列之中,讓副作用因應他們的變化而重新觸發。

現在,讓我們從例子來看看 useEffectEvent 的用法吧。

import { useEffect } from "react";

export default function ChatRoom({ url, loggingOptions }) {
function onConnected(url){
logConnection(`已連結至 ${url}`)
};

useEffect(() => {
const room = connectToRoom(url);
room.onConnected(() => {
onConnected(url)
})

return () => {
room.disconnect()
}
}, [url]);

// ...
};

這是一個聊天室的元件,當元件掛載,我們透過從父元件拿到的 url ,在 useEffect 來執行連線到聊天室(connectToRoom)的副作用,並在連線時,執行在元件定義的 onConnected 邏輯,記錄一些連線資訊(logConnection)。

目前這段程式碼是 OK 的,但如果我們希望將從 props 拿到的 loggingOptions 放到 logConnection,作為它的第二個參數的話,亦即:

function onConnected(url) {
logConnection(`已連結至 ${url}`, loggingOptions);
};

但是,我們的需求是不希望副作用依賴這個 loggingOptions ,只想要它根據 url 是否改變,決定是否重新執行。

不過當 loggingOptions 成為 logConnection 的參數後,由於 onConnected 現在需要用到 props 的 loggingOptions,Eslint 將會要求把 onConnected 放入依賴項之中,即:

useEffect(() => {
const room = connectToRoom(url);
room.onConnected(() => {
onConnected(url);
});

return () => {
room.disconnect();
};
}, [url, onConnected]);

接著,又因為我們將函式放入依賴項,會遇上上述提過的函式參考問題,Eslint 將會再次發出警告,並提供兩種解法:

A、直接將 onConnected 放入副作用中,此時因為 onConnected 會用到 props 的 loggingOptions,我們必須把 loggingOptions 加到依賴項:

useEffect(() => {
function onConnected(url) {
logConnection(`已連結至 ${url}`, loggingOptions);
};

const room = connectToRoom(url);
room.onConnected(() => {
onConnected(url);
});

return () => {
room.disconnect();
};
}, [url, loggingOptions]); // 👈 因為 onConnected 用到 loggingOptions,必須將它放到依賴陣列中

B、用 useCallback 穩定 onConnected 的參考,但即使使用 useCallback,因為 logConnection 用到了 loggingOptions,我們同樣必須將 loggingOptions 放進 useCallback 的依賴陣列,然後發現結果還是一樣:

loggingOptions 改變 → 重新執行 onConnected 函式 → onConnected 參考改變 → 觸發副作用
const onConnected = useCallback((url) => {
logConnection(`已連結至 ${url}`, loggingOptions)
}, [loggingOptions]);
// 👆 因為 useCallback 裡的 callback 用到 loggingOptions

useEffect(() => {
const room = connectToRoom(url);
room.onConnected(() => {
onConnected(url);
});

return () => {
room.disconnect();
};
}, [url, onConnected]);

如果我們不希望這個副作用因 loggingOptions 的改變而重新觸發,此時就可以用上 useEffectEvent 。讓我們來看看這個新 Hook 的語法:

const onSomething = useEffectEvent(callback)

我們要做的只需要將原本的 onConnected 函式邏輯,作為 useEffectEvent 的參數傳入,再將其回傳的函式,用於 useEffect 之中即可。

const onConnected = useEffectEvent((url) => {
logConnection(`已連結至 ${url}`, loggingOptions)
});

useEffect(() => {
const room = connectToRoom(url);
room.onConnected(() => {
onConnected(url);
});

return () => {
room.disconnect();
};
}, [url]); // 不需要傳入 onConnected

可以把 useEffectEvent 的 effect event 理解成 useEffect 內部使用的事件處理器,類似 onClick 之於 DOM event 的角色,它負責定義「當副作用實際發生時,要執行什麼邏輯」,兩者不存在依賴關係,因此不需要,也不應該將它放進 useEffect 的依賴陣列中。

在上方的改寫中,副作用會因為 url 的改變而重新觸發,就不會對 loggingOptions 的改變而再次執行。

loggingOptions 包進 useEffectEvent 當中,永遠可以讀取到它的最新值,而不會觸發副作用。而傳入 onConnected 的參數,也就是 url,由於它是副作用觸發的依賴,可以將其傳入 effect event 之中,作為這次事件發生的資料,確保兩者讀到的是一致的。

總結

回顧一下關於 useEffect 的重點:

  • useEffect 是用來與外部系統同步、執行副作用的工具,包含三個部分:副作用邏輯、清理函式以及依賴項。
  • useEffect 會在元件掛載、任一依賴改變時觸發,執行時 DOM 已經完成繪製。
  • 清理函式會在元件卸載前、副作用重新觸發前執行。
  • 依賴項是陣列,決定副作用的觸發時機。
  • 常見的使用情境包括抓取資料、訂閱事件、與瀏覽器 API 互動等。
  • 如果希望副作用與 DOM 同步觸發,請使用 useLayoutEffect
  • 如果能在渲染過程中計算,就可能不需要放在副作用中做。
  • 如果將物件資料放到依賴項中,要注意參考改變的問題。
  • 如果希望讀取最新的值,但不希望這些值觸發副作用,可以將邏輯放進 useEffectEvent

參考資料

  1. React 官方文件
  2. Learn useEffect In 13 Minutes
  3. New React useEffectEvent Hook Crash Course
  4. This New React Hook Finally Fixes useEffect
留言
avatar-img
肝 code 人生
1會員
6內容數
2024 年 7 月開始的「肝 code 人生」,2025 年 1 月撰寫第一篇程式筆記
你可能也想看
Thumbnail
在 vocus 與你一起探索內容、發掘靈感的路上,我們又將啟動新的冒險——vocus App 正式推出! 現在起,你可以在 iOS App Store 下載全新上架的 vocus App。 無論是在通勤路上、日常空檔,或一天結束後的放鬆時刻,都能自在沈浸在內容宇宙中。
Thumbnail
在 vocus 與你一起探索內容、發掘靈感的路上,我們又將啟動新的冒險——vocus App 正式推出! 現在起,你可以在 iOS App Store 下載全新上架的 vocus App。 無論是在通勤路上、日常空檔,或一天結束後的放鬆時刻,都能自在沈浸在內容宇宙中。
Thumbnail
vocus 慶祝推出 App,舉辦 2026 全站慶。推出精選內容與數位商品折扣,訂單免費與紅包抽獎、新註冊會員專屬活動、Boba Boost 贊助抽紅包,以及全站徵文,並邀請你一起來回顧過去的一年, vocus 與創作者共同留下了哪些精彩創作。
Thumbnail
vocus 慶祝推出 App,舉辦 2026 全站慶。推出精選內容與數位商品折扣,訂單免費與紅包抽獎、新註冊會員專屬活動、Boba Boost 贊助抽紅包,以及全站徵文,並邀請你一起來回顧過去的一年, vocus 與創作者共同留下了哪些精彩創作。
Thumbnail
這篇文章整理了前端開發中常見的效能優化技巧、React與JavaScript的知識點,以及Redux Toolkit和React Fiber的應用、Reflow與Repaint、Event Loop、Higher Order Component、React Hooks等主題。
Thumbnail
這篇文章整理了前端開發中常見的效能優化技巧、React與JavaScript的知識點,以及Redux Toolkit和React Fiber的應用、Reflow與Repaint、Event Loop、Higher Order Component、React Hooks等主題。
Thumbnail
最近工作上可能要用 React + Tailwind 了,剛好正巧遇到 React 19 和 Tailwind 4.0 剛推出,尤其是 Tailwind 做了大改版,對我這樣剛好是這兩項技術的新手小白來說,還沒有更多文章可以參考,光是安裝也是摸索了一陣子。以下以 Vite 6 + React 18.
Thumbnail
最近工作上可能要用 React + Tailwind 了,剛好正巧遇到 React 19 和 Tailwind 4.0 剛推出,尤其是 Tailwind 做了大改版,對我這樣剛好是這兩項技術的新手小白來說,還沒有更多文章可以參考,光是安裝也是摸索了一陣子。以下以 Vite 6 + React 18.
Thumbnail
這一集用最新的Vite工具去創建初始檔案。Vite用於創建和構建Web應用程序,具有快速的啟動時間、即時熱更新、小型體積、支持多種框架和可擴展性等優點。
Thumbnail
這一集用最新的Vite工具去創建初始檔案。Vite用於創建和構建Web應用程序,具有快速的啟動時間、即時熱更新、小型體積、支持多種框架和可擴展性等優點。
Thumbnail
使用React.js實作CAPTCHA元件的步驟和技巧
Thumbnail
使用React.js實作CAPTCHA元件的步驟和技巧
Thumbnail
Storybook 是一個用來透過獨立元件快速開發 UI 介面的工具,以往要開發元件時,我們可能需要建立一個全新的頁面才能進行開發,但這樣的開發方式可能會有一個狀況:沒有辦法事先開發或是預覽流程中不存在的元件。 透過 Storybook 我們在開發元件時,不需要重新建立複雜的頁面結構⋯⋯
Thumbnail
Storybook 是一個用來透過獨立元件快速開發 UI 介面的工具,以往要開發元件時,我們可能需要建立一個全新的頁面才能進行開發,但這樣的開發方式可能會有一個狀況:沒有辦法事先開發或是預覽流程中不存在的元件。 透過 Storybook 我們在開發元件時,不需要重新建立複雜的頁面結構⋯⋯
Thumbnail
在 2021 年的剛轉職成為前端工程師的時候,我在面試時滿常會被詢問到 JavaScript 中閉包的議題,當時候自己回答的滿差的,於是在 2022 年時,我寫了一系列的有關於函式程式設計鐵人賽的文章, 裡頭就有簡單提到有關於閉包的議題。
Thumbnail
在 2021 年的剛轉職成為前端工程師的時候,我在面試時滿常會被詢問到 JavaScript 中閉包的議題,當時候自己回答的滿差的,於是在 2022 年時,我寫了一系列的有關於函式程式設計鐵人賽的文章, 裡頭就有簡單提到有關於閉包的議題。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News