useEffect 其實不是 function component 的生命週期 API
useEffect 是宣告式的同步化,而非生命週期 API
什麼是「宣告式的同步化」?
在 React 中,useEffect 的設計並不是為了直接模擬類別元件(class component)的生命週期方法(例如 componentDidMount、componentDidUpdate、componentWillUnmount),而是用來讓你的副作用(side effect)與元件的狀態或屬性(props)保持同步。它的核心理念是「宣告式」的:你告訴 React 你想要在什麼條件下執行什麼副作用,React 會負責在適當的時機執行它。
具體來說:
-
副作用(side effect):像是資料請求(fetch data)、訂閱事件、操作 DOM 等,這些操作不在 React 的控制範圍內。
-
宣告式:你只需要定義「什麼時候(依賴項改變時)要做什麼(執行某個副作用)」,React 會自動根據元件的渲染時機來調度這些副作用。
-
同步化:useEffect 確保你的副作用與元件當前的狀態或屬性保持一致。例如,當某個狀態改變時,你可能需要重新發送 API 請求來獲取新資料。
這與類別元件的生命週期方法不同,後者是命令式的(imperative),你需要明確指定在「某個生命週期階段」做什麼(例如「掛載時」或「更新時」)。useEffect 則更專注於「資料與副作用的同步」,而不是特定的生命週期階段。
程式碼範例:展示宣告式的同步化
假設你正在開發一個顯示使用者資料的元件,當 userId 改變時,需要重新從 API 獲取資料。
import React, { useState, useEffect } from "react";
function UserProfile({ userId }) {
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 定義副作用:當 userId 改變時,發送 API 請求
async function fetchUserData() {
setLoading(true);
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
setUserData(data);
} catch (error) {
console.error("錯誤:", error);
} finally {
setLoading(false);
}
}
fetchUserData();
}, [userId]); // 當 userId 改變時,重新執行副作用
if (loading) {
return <div>載入中...</div>;
}
return (
<div>
<h1>{userData?.name}</h1>
<p>{userData?.email}</p>
</div>
);
}
export default UserProfile;
範例解析
-
宣告式思維:
-
你在 useEffect 中宣告:「當 userId 改變時,執行 fetchUserData 來更新資料。」
-
你不需要明確指定「這是在掛載時執行」或「這是在更新時執行」,React 會根據 userId 的變化自動決定什麼時候執行。
-
-
同步化:
- useEffect 確保 userData 與 userId 保持同步。只要 userId 改變,useEffect 就會重新執行,觸發 API 請求,更新 userData。
-
非生命週期:
- 這裡並沒有明確的「掛載」或「更新」階段,useEffect 的執行完全由依賴項 [userId] 驅動。這與類別元件的 componentDidMount 或 componentDidUpdate 的命令式思維不同。
與類別元件的對比
在類別元件中,你可能會這樣寫:
import React, { Component } from "react";
class UserProfile extends Component {
state = {
userData: null,
loading: true,
};
componentDidMount() {
this.fetchUserData(this.props.userId);
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUserData(this.props.userId);
}
}
async fetchUserData(userId) {
this.setState({ loading: true });
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data = await response.json();
this.setState({ userData: data });
} catch (error) {
console.error("錯誤:", error);
} finally {
this.setState({ loading: false });
}
}
render() {
if (this.state.loading) {
return <div>載入中...</div>;
}
return (
<div>
<h1>{this.state.userData?.name}</h1>
<p>{this.state.userData?.email}</p>
</div>
);
}
}
export default UserProfile;
對比分析:
-
類別元件需要你明確區分「掛載」(componentDidMount)和「更新」(componentDidUpdate),並手動比較
prevProps.userId
與this.props.userId
是否不同。 -
useEffect 則簡化了這個過程:你只需要指定依賴項 [userId],React 會自動在適當的時機執行副作用。
Dependencies 是一種效能優化,而非執行時機的控制
什麼是 Dependencies 的角色?
useEffect 的第二個參數(依賴項陣列,dependency array)是用來控制副作用的執行頻率,以避免不必要的重複執行,從而達到效能優化的目的。它並不是用來「精確控制執行時機」(例如「只在掛載時執行」),而是告訴 React:「只有當這些依賴項改變時,才需要重新執行副作用。」
為什麼說是效能優化?
-
如果沒有依賴項陣列(或傳入空陣列 []),useEffect 會在每次渲染後都執行,這可能導致不必要的副作用執行(例如重複的 API 請求)。
-
通過正確設定依賴項陣列,你可以限制副作用只在「必要的時候」執行,減少無謂的計算或網路請求。
-
依賴項陣列的本質是 React 用來比較前後渲染的依賴值是否改變,進而決定是否執行副作用。
程式碼範例:展示依賴項的效能優化
假設你有一個計數器元件,當計數改變時,你想記錄到日誌(log),但只有在特定條件下才執行。
import React, { useState, useEffect } from "react";
function Counter() {
const [count, setCount] = useState(0);
const [userId, setUserId] = useState(1);
// 副作用:記錄計數到日誌
useEffect(() => {
console.log(`計數改變,現在是 ${count}`);
// 假設這是一個發送到後端的日誌記錄
fetch("https://api.example.com/log", {
method: "POST",
body: JSON.stringify({ count, userId }),
});
}, [count, userId]); // 只有 count 或 userId 改變時才執行
return (
<div>
<p>目前計數: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
<button onClick={() => setUserId(userId + 1)}>切換使用者</button>
</div>
);
}
export default Counter;
範例解析
-
依賴項的作用:
-
useEffect 的依賴項是
[count, userId]
,這意味著只有當 count 或 userId 改變時,副作用(記錄日誌)才會執行。 -
如果你點擊「增加」按鈕,count 改變,副作用執行;如果你點擊「切換使用者」,userId 改變,副作用也執行。
-
如果元件因為其他原因重新渲染(例如父元件傳入不相關的 props),但 count 和 userId 沒變,副作用就不會執行,這就是效能優化。
-
-
錯誤用法:沒有依賴項: 如果你移除依賴項陣列
(useEffect(() => {...}))
,副作用會在每次渲染後都執行,這可能導致不必要的日誌記錄或 API 請求,浪費資源。 -
錯誤用法:空陣列: 如果你傳入空陣列
(useEffect(() => {...}, \[\]))
,副作用只會在元件掛載時執行一次,但如果 count 或 userId 改變,副作用不會重新執行,這可能導致資料不同步。
常見誤解:依賴項控制執行時機
有些人誤以為依賴項陣列是用來「控制副作用在什麼時候執行」(例如模擬 componentDidMount)。這是不正確的。依賴項的真正目的是限制不必要的副作用執行,而不是強制指定某個生命週期階段。以下是常見的錯誤用法:
// 錯誤:試圖模擬 componentDidMount
useEffect(() => {
console.log("這只在掛載時執行");
}, []); // 空陣列只在掛載時執行,但這不是 useEffect 的主要目的
// 正確:應該專注於同步化
useEffect(() => {
console.log("當 userId 改變時,執行某些副作用");
}, [userId]); // 專注於 userId 的同步
總結與實務建議
-
useEffect 是宣告式的同步化:
-
它的核心是讓副作用與狀態/屬性保持同步,而不是模擬生命週期。
-
透過宣告依賴項,你告訴 React 什麼時候需要重新執行副作用,無需手動管理生命週期。
-
-
依賴項是效能優化工具:
-
依賴項陣列的目的是避免不必要的副作用執行,而不是精確控制執行時機。
-
正確設定依賴項可以顯著提升應用程式的效能,避免浪費資源。
-
-
實務建議:
-
總是明確列出所有在 useEffect 中使用的狀態或屬性作為依賴項,避免資料不同步。
-
使用 ESLint 的 exhaustive-deps 規則,確保依賴項陣列正確無誤。
-
如果需要清理副作用(例如取消訂閱或清除計時器),在 useEffect 中回傳一個清理函數:
useEffect(() => {
const timer = setInterval(() => {
console.log("計時器執行中");
}, 1000);
return () => clearInterval(timer); // 清理副作用
}, []); -