剛開始學習 React 的時候,我覺得 useEffect 就像「魔法一樣」,從資料獲取與更新、追蹤使用者行為,到訂閱連線⋯⋯,這些讓畫面更有活力的功能背後,幾乎都與 useEffect 有關。
不過,useEffect 固然強大萬能,卻又時常令人摸不著頭緒,究竟它司職什麼?在元件的生命週期中扮演怎樣的角色?這篇文章會從它的定義開始,一步步探索它的常見用法、容易踩雷的地方,以及為什麼 React 官方文件會說:「你也許不需要 useEffect」。
useEffect 做了什麼?
在沒有 useEffect 的世界中,React 的函式元件主要只負責一件事:
根據狀態與 props,計算並繪製出呈現在使用者眼前的介面。
這是 React 的設計原則:元件的函式應該要是「純的」(pure function),也就是每次執行,都會渲染出同樣的畫面結果。但實務上的需求,往往更加複雜,時常需要與「外部系統」(external system) 互動,比如說:
- 瀏覽器 API(DOM、
window、document⋯⋯) - 網路(fetch、WebSocket⋯⋯)
- 第三方套件(地圖、播放器、圖表庫⋯⋯)
- 訂閱型系統(Timer、Event Emitter、Observer⋯⋯)
我們會將這些互動產生的行為稱為「副作用」。外部系統在 React 掌控範圍之外,因此,我們需要在元件渲染完成後,很明確地告訴 React,是否要與外部系統連線、什麼時候要斷開,而 useEffect 就是在其中扮演同步的角色。
簡單來說,我們可以簡單地用這句話來理解 useEffect :
當畫面更新完成後,如果條件成立,就去做這些事。
如何撰寫 useEffect?
以下是 useEffect 的基本語法:
useEffect(effectFn, dependencies?)
useEffect 本身不回傳值,它接受兩個參數:
effectFn:執行副作用邏輯的回調函式,包含兩個部分——
a. 每次觸發時運行的副作用邏輯,至少會在元件掛載時運行一次
b. 可選的清理函式 (cleanup),如需使用,放在 return 後方作為 effectFn 回傳的函式,它會在元件卸載,以及每次副作用重新執行前觸發。dependencies:可選的依賴項,決定副作用觸發的時機,它有三種情況——
a. 不傳入:副作用每次渲染時都會執行,但實務上很少這麼做
b. 空陣列[]:只在元件掛載時執行
c. 有值,即[dep1, dep2, ...]的狀況:通常是狀態或 props,只要陣列中任一值改變,就會執行清理函式(如果有的話),接著重新執行副作用。
值得注意的是,雖然我們擁有將哪些值當作依賴項的最終決定權,但如果副作用邏輯(effectFn)包含狀態、props 等會觸發重新渲染的值的話,React 官方建議務必將它們放入依賴陣列中。
最後,讓我們將 useEffect 展開,基本上會看到這樣的結構:
useEffect(() => {
// 主要的副作用邏輯
return () => {
// 清理函式邏輯(可選)
}
}, [dep1, dep2, ...]) // 依賴項
React 元件的生命週期
如果你對上述那些掛載、卸載、渲染等名詞感到陌生,你並不孤單,我也是這樣,就算是這樣,依然還是可以寫出能運作的元件。不過話雖如此,理解執行順序,我認為還是十分重要的。
想像元件的生命週期分成三個階段:
- 掛載階段 (mount) ——元件首次被加到 DOM 上的過程。
執行元件函式 → 根據 JSX 建立虛擬 DOM →
加到真實 DOM → 畫面完成繪製 → 執行 useEffect。 - 更新階段 (update) ——當 state、props 或 context 改變時,元件重新渲染的過程。
執行元件函式 → 建立新的虛擬 DOM → 比對新舊虛擬 DOM 差異 →
提交更新到真實 DOM → 畫面完成繪製 →
若 useEffect 依賴項有變動,先執行清理函式,再重新執行。 - 卸載階段 (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)的狀況,實務上,可能會更常看到使用AbortControllerAPI 來中斷請求(詳見: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>
);
}
setInterval會回傳一個識別碼,而這個識別碼正是需要在clearInterval的參數傳入的值,因此將它存在自定義的變數intervalId中,作為清理函數的clearInterval依據。- 如果是撰寫
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),且會在 firstName 或 lastName 改變後,在處理副作用時,因為 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」的狀況主要可以往這些方向思考:
- 是否在副作用中觸發了重新渲染(
setState)? - 是否是在副作用處理了衍生狀態的計算?
- 資料的流動是否過於複雜?
如果這些操作能在渲染的過程中完成,它可能就不需要 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:
要解決這種狀況,可以直接將這些依賴放在副作用定義,避免將其放入依賴,進而減少依賴陣列的長度;若需要在他處使用,可以分別使用 useMemo 及 useCallback 來穩定物件及函式的參考。
✅ 用 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。













