前端框架
-useEffect 教學 - React 的副作用管理
this.web

什麼是 useEffect?
useEffect 是 React 中非常重要的一個 Hook,它讓函數組件(functional component)可以執行副作用 (Side Effects)。
但工作這幾年,我發現有許多工程師並不是真的理解 useEffect
的機制以及含義,所以這篇文章,會從 0 開始帶你徹底了解 useEffect。
useEffect 的語法
useEffect 的語法不難,像這樣:
useEffect(setupFn, dependencies?)
這個 Hook 接收兩個參數:一個是副作用邏輯的函式 setupFn,另一個是選填的依賴陣列 dependencies。
- setupFn:這是一個函式,當組件完成渲染後會執行這個函數,這裡就是撰寫副作用邏輯的地方。這個函數也可以選擇性地回傳另一個函式,也就是 cleanup function,這個函數會在下一次副作用執行之前或元件卸載時被呼叫,用於清除副作用,例如移除事件監聽器或清除計時器。
- dependencies:是一個陣列,用來告訴 React 當哪些值發生變化時,應該重新執行 setupFn。
- 如果省略這個陣列,副作用會在每次渲染後都執行。
- 若提供空陣列
[]
,副作用只會在組件初次掛載時執行一次。 - 若陣列中包含特定的變數(如 [count, userId]),副作用只會在這些變數的值改變時才重新執行。
所以實務上,useEffect 通常會這樣去寫:
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// setup logic
return () => {
// return 一個 function 作為 cleanup function
};
}, []); // 這裡可以省略陣列、提供空陣列、提供特定變數
return (...);
}
透過這兩個參數的搭配,我們就可以透過 useEffect 靈活地控制副作用的執行時機,並且提供清理機制。
了解 useEffect 之前要先知道的 Side Effect (副作用)
在文章一開始就提到了副作用,不過對於程式新手來說可能會沒聽過這個詞,但要使用好 useEffect 勢必要先理解副作用是什麼。
什麼是副作用,簡單說:副作用就是這個函數會影響外部環境,這邊我舉一個很簡單的例子:
// 這個函數有副作用 - 它會修改全域變數
let counter = 0;
function increment() {
counter++; // 副作用:修改了外部的 counter 變數
return counter;
}
console.log(increment()); // 1
console.log(increment()); // 2
//因為改變了全域變數 counter,導致相同的呼叫會有不同的結果
比如有個全域變數 counter 以及一個函數 increment()
,我們在 increment 裡面去修改全域變數 counter,這就讓這個函數有副作用了,因為他影響了外部環境。
這可能會讓程式碼變得難以維護,因為你不清楚某些全域變數是不是被改變過了。
當一個函數符合 2 個條件,我們就稱它為 Pure Function(純函數、函式)
- 相同輸入永遠得到相同輸出
- 沒有副作用
我們跟上個例子比較一下,可以發現 add 這個函數只要傳入的值相同,輸出也永遠會相同,除此之外,他也沒有改變任何的外部環境,所以這就是一個純函數
// 這是一個純函數
function add(a, b) {
return a + b;
}
// 每次呼叫都會得到相同的結果
console.log(add(2, 3)); // 永遠是 5
console.log(add(2, 3)); // 永遠是 5
透過這個 add
的例子,我們可以比對一般的 React Component,你會發現他也是一個 Pure Function,比如這個 Counter
function Counter(initCount) {
const [count, setCount] = useState(initCount);
return (
<div>
<p>{count}</p>
</div>
)
}
相同的輸入(initCount),永遠會是相同的輸出,也沒有任何副作用,這裡的 state 只會影響函數內部,和外部沒有關聯。這也是 React 的核心思維之一 - Pure Function。
useEffect 的核心概念
當了解什麼是 Pure Function,並發現一般的 React Component 就是所謂的 Pure Function 後,我們就能理解 useEffect
的核心概念了。
因為我們的程式一定會有副作用,比如:
- 發送網路請求取得資料
- 訂閱外部資料流
- 操作瀏覽器 API(如文件標題或
localStorage
) - 註冊或移除事件監聽
- 定時器的設置和清除 … 等等
為了讓我們能處理這些副作用,useEffect
誕生了!useEffect
的核心概念在於管理程式中的副作用,只要是副作用,就能用 useEffect 處理。
useEffect 的執行時機 - 何時開始、何時清理?
要理解 useEffect
的運作方式,首先要知道 React 在每次更新(也就是重新渲染組件)時,會經過兩個主要階段:
- Render Phase(渲染階段):React 會重新執行組件函數,產生新的 Virtual DOM。這個階段只是在計算畫面應該長什麼樣子,實際上還沒有改動真實畫面。
- Commit Phase(提交階段):React 將計算出的變化應用到真實的 DOM,也就是這個時候畫面才真正被更新。
useEffect
的副作用函式(也就是你傳進 useEffect
裡的那個函式)會在 Commit Phase 結束後執行。這代表副作用的執行時間點是畫面更新完成之後,這樣可以確保你操作的是已經呈現在畫面上的元素。
此外,如果你在 useEffect
裡面回傳一個清理函數(Cleanup Function),React 會在執行下一次 effect 前後值行清理函數
簡單來說,每次 useEffect
重新執行前,React 都會先清除前一次的副作用,這可以避免訂閱、計時器或事件監聽累積重複。
這個機制非常重要,它能幫助我們控制副作用的生命週期,確保程式不會重複執行相同操作,也不會留下未清除的事件或資源。對前端新手來說,理解這點是寫出穩定 React 應用的基礎。
延伸閱讀:
useEffect 的實際範例 - 獲取資料
當我們想要在 React 組件中載入資料時(例如從 API 抓取 JSON),useEffect
是最常用的工具之一。因為資料請求通常是一種副作用,它發生在畫面渲染之外,必須等組件「出現在畫面上」之後才能進行。
在這種情況下,我們可以透過 useEffect
來達成以下流程:
- 組件初次掛載後執行資料請求
- 請求完成後,透過
setState
將資料存入狀態中 - React 會根據資料更新重新渲染畫面
- (可選)當元件卸載時取消請求或清理資源
import { useEffect, useState } from 'react';
export function UserList() {
const [users, setUsers] = useState([]); // 儲存 API 回傳的使用者資料
const [loading, setLoading] = useState(true); // 資料是否載入中的狀態
const [error, setError] = useState(null); // 錯誤訊息(若發生錯誤)
useEffect(() => {
// 定義 async 函式來抓取資料
const fetchUsers = async () => {
try {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
const data = await res.json();
setUsers(data); // 儲存資料到 state
} catch (err) {
setError(err.message); // 設定錯誤訊息
} finally {
setLoading(false); // 載入完成
}
};
fetchUsers();
}, []); // 空陣列代表只在組件初次掛載時執行一次
// 渲染畫面
if (loading) return <p>載入中...</p>;
if (error) return <p>錯誤:{error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name}({user.email})
</li>
))}
</ul>
);
}
這種資料抓取的模式非常常見,例如在組件載入時從 REST API 取得文章列表、使用者資訊、天氣資料等等。
通常我們會搭配空的依賴陣列 []
,表示這個副作用只在組件掛載時執行一次。這樣可以避免每次重新渲染都重複發送請求。
要注意的是,如果請求資料需要依賴某個變數(例如根據 userId
抓取個別使用者的資料),那就必須把該變數加進依賴陣列,確保當 userId
改變時重新取得正確的資料。
useEffect(() => {
if (!userId) return; // 如果 userId 是 undefined 或 null,則不執行
const fetchUser = async () => {
setLoading(true);
setError(null);
try {
// 👇 根據 userId 抓取資料
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
const data = await res.json();
setUser(data);
} catch (err) {
setError(err.message);
setUser(null);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // 依賴 userId,當它變化時重新執行 effect
這種用法是前端開發中最常見的副作用範例之一,幾乎每個需要與伺服器互動的應用都會使用到。所以說,熟悉這個模式,就能幫助你建立基本但重要的資料流處理能力。
爲什麼很多人覺得 useEffect 不好用?
有非常多的工程師會將 useEffect
視為類似 Class Component 的生命週期的替代品。
在 Hook 出來之前,我們有一組明確的生命週期函式,比如 componentDidMount
、componentDidUpdate
和 componentWillUnmount
。這些函數提供了一個明確的時機點來執行某些操作。
而當 React 推出 useEffect
Hook 後,很多工程師自然會把它對應到 class component 的這些生命週期上。很多人對 effect 的想法就是在組件掛載或卸載後後執行某些程式。像這樣:
useEffect(() => {
console.log('Component did mount or userId changed');
return () => {
console.log('Cleanup before unmount or before userId changes again');
};
}, [userId]);
這種理解方式雖然在某些情況下能幫助我們快速上手,但實際上是限制我們對 useEffect
的認知。
使用 useEffect 的注意事項
一、避免無限循環
當你沒有正確地設置依賴陣列時,可能會導致 useEffect
不斷觸發,造成無限循環。通常發生在你在 useEffect
中更新狀態,且將這個狀態作為 useEffect 依賴的時候。
useEffect(() => {
setCount(count + 1); // 這樣會導致無限循環
}, [count]);
二、我的 effect 執行 2 次
原因是你使用了 React Strict Mode。
React 18 開始,當你使用 <React.StrictMode>
包住應用程式時,React 會在開發模式下針對某些 Hook 進行額外的模擬執行,包含:
useEffect
useLayoutEffect
這是 React 為了找出不安全副作用的檢查機制。
React 會先執行一次你的組件和其副作用,然後立刻模擬卸載並重新掛載組件,觸發第二次 effect 的執行,這是為了確保這兩次掛載的時的副作用有被清除。
但這個只在開發模式下發生,所以不用擔心,如果你不希望 React 執行 2 次 effect,可以把 <React.StrictMode>
拿掉就好。
三、違反 Hooks 調用規則
和其他的 Hook 一樣,useEffect 不能在循環、條件語句或嵌套函數中使用。
因為 React 依賴 Hook 的調用順序來管理狀態。每次渲染時,React 都會按照固定順序執行所有的 Hook,如果 Hook 的順序發生改變,React 將無法正確追蹤狀態,可能導致狀態錯亂或錯誤。
四、將不是副作用的邏輯放到 useEffect
像上個小節提到的,很多工程師會將 useEffect 視為生命週期,於是把無關副作用的邏輯放進來裡面,這是錯誤的做法,某些時候會照成意料之外的 Bug 或效能問題,例如一個打開搜尋框的例子,我們希望在打開搜尋框後,初始其他 state:
useEffect(() => {
if (isOpen) {
setSearch('');
setResults([]);
}
}, [isOpen]);
但這樣會照成額外的 Re-render,正確做法是將邏輯放到事件中處理:
const handleOpenChange = (open) => {
setIsOpen(open);
if (open) {
setSearch('');
setResults([]);
}
};
useEffect 的總結
useEffect
是 React 函數組件中處理副作用(Side Effects)的核心工具,幾乎每一個與外部互動的應用場景(如資料請求、事件監聽、DOM 操作)都會用到它。
透過 useEffect
,我們就能在畫面渲染完成後有條理地管理副作用,並根據依賴條件自動更新,同時也能清理資源。