第 6 章:元件生命週期與 useEffect
Hook
在前面的章節中,我們學習了如何使用 Props 傳遞資料以及如何使用 State 管理元件內部的動態資料。但是,當我們需要在元件的特定時機執行某些操作時,例如當元件首次載入時獲取資料,或當元件被移除時清理資源,我們就需要了解 React 元件的「生命週期」(Component Lifecycle)。
在現代 React 中,我們主要使用 useEffect
Hook 來處理這些生命週期相關的邏輯。
6.1 什麼是元件生命週期?
React 元件從被建立到被銷毀會經歷一系列的階段,這些階段被稱為元件的「生命週期」。主要的生命週期階段包括:
- 掛載 (Mounting):元件被建立並插入到 DOM 中
- 更新 (Updating):元件的 Props 或 State 發生變化,需要重新渲染
- 卸載 (Unmounting):元件從 DOM 中被移除
在每個階段,我們可能需要執行不同的操作:
- 掛載時:獲取資料、設定訂閱、初始化計時器
- 更新時:響應 Props 或 State 的變化、重新計算值
- 卸載時:清理訂閱、取消計時器、釋放資源
6.2 useEffect
Hook 簡介
useEffect
是 React 提供的一個 Hook,讓我們可以在函式元件中執行「副作用」(side effects)。副作用是指那些不直接與元件渲染相關的操作,例如:
- API 呼叫
- 訂閱事件
- 手動修改 DOM
- 設定計時器
- 記錄日誌
基本語法:
import React, { useEffect } from 'react';
useEffect(() => {
// 副作用程式碼
});
6.3 useEffect
的基本使用
1. 在每次渲染後執行:
最簡單的 useEffect
用法是不提供第二個參數,這樣 effect 會在每次元件渲染後執行。
import React, { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
// 在每次渲染後執行
useEffect(() => {
document.title = `計數: ${count}`;
console.log('元件已渲染,目前計數:', count);
});
return (
<div>
<p>目前計數: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}
export default Counter;
在這個例子中,每當 count
狀態改變導致元件重新渲染時,useEffect
中的程式碼都會執行,更新瀏覽器標籤頁的標題。
2. 只在元件掛載時執行(模擬 componentDidMount
):
如果我們只想在元件首次掛載時執行某些操作,可以提供一個空的依賴陣列 []
。
import React, { useState, useEffect } from 'react';
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// 只在元件掛載時執行一次
useEffect(() => {
console.log('元件已掛載,開始獲取使用者資料...');
// 模擬 API 呼叫
setTimeout(() => {
setUser({ name: 'React 使用者', email: 'user@react.dev' });
setLoading(false);
}, 2000);
}, []); // 空的依賴陣列表示這個 effect 只會執行一次
if (loading) {
return <div>載入中...</div>;
}
return (
<div>
<h2>使用者資料</h2>
<p>姓名: {user.name}</p>
<p>信箱: {user.email}</p>
</div>
);
}
export default UserProfile;
6.4 依賴陣列 (Dependency Array)
useEffect
的第二個參數是「依賴陣列」,它決定了 effect 何時重新執行。
語法:
useEffect(() => {
// effect 程式碼
}, [dependency1, dependency2, ...]); // 依賴陣列
不同的依賴陣列行為:
沒有依賴陣列:effect 在每次渲染後執行
useEffect(() => { console.log('每次渲染後執行'); });
空的依賴陣列:effect 只在掛載時執行一次
useEffect(() => { console.log('只在掛載時執行一次'); }, []);
有依賴的陣列:effect 在依賴值改變時執行
useEffect(() => { console.log('當 count 改變時執行'); }, [count]);
範例:監聽特定狀態的變化
import React, { useState, useEffect } from 'react';
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
// 只有當 searchTerm 改變時才執行搜尋
useEffect(() => {
if (searchTerm.length > 0) {
console.log(`搜尋: ${searchTerm}`);
// 模擬搜尋 API 呼叫
setTimeout(() => {
setResults([`結果 1 for ${searchTerm}`, `結果 2 for ${searchTerm}`]);
}, 500);
} else {
setResults([]);
}
}, [searchTerm]); // 只有當 searchTerm 改變時才重新執行
return (
<div>
<input
type="text"
placeholder="輸入搜尋詞..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<ul>
{results.map((result, index) => (
<li key={index}>{result}</li>
))}
</ul>
</div>
);
}
export default SearchComponent;
6.5 清理函數 (Cleanup Function)
有時候,我們需要在元件卸載或 effect 重新執行之前清理一些資源,例如取消訂閱、清除計時器等。我們可以從 useEffect
中返回一個「清理函數」。
語法:
useEffect(() => {
// 設定副作用
return () => {
// 清理函數
};
}, [dependencies]);
範例 1:清理計時器
import React, { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
console.log('設定計時器');
const interval = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
// 清理函數:在元件卸載或依賴改變時清除計時器
return () => {
console.log('清除計時器');
clearInterval(interval);
};
}, []); // 空依賴陣列,計時器只設定一次
return (
<div>
<h2>計時器: {seconds} 秒</h2>
</div>
);
}
function App() {
const [showTimer, setShowTimer] = useState(true);
return (
<div>
<button onClick={() => setShowTimer(!showTimer)}>
{showTimer ? '隱藏' : '顯示'} 計時器
</button>
{showTimer && <Timer />}
</div>
);
}
export default App;
範例 2:清理事件監聽器
import React, { useState, useEffect } from 'react';
function WindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
console.log('添加 resize 事件監聽器');
window.addEventListener('resize', handleResize);
// 清理函數:移除事件監聽器
return () => {
console.log('移除 resize 事件監聽器');
window.removeEventListener('resize', handleResize);
};
}, []); // 只在掛載時添加監聽器,卸載時移除
return (
<div>
<h2>視窗大小</h2>
<p>寬度: {windowSize.width}px</p>
<p>高度: {windowSize.height}px</p>
</div>
);
}
export default WindowSize;
6.6 使用多個 useEffect
一個元件可以擁有多個 useEffect
,用於處理不同的副作用。這有助於將相關的邏輯分組,使程式碼更容易理解和維護。
import React, { useState, useEffect } from 'react';
function UserDashboard({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [onlineStatus, setOnlineStatus] = useState(false);
// Effect 1: 獲取使用者資料
useEffect(() => {
console.log('獲取使用者資料 for userId:', userId);
// 模擬 API 呼叫
setTimeout(() => {
setUser({ id: userId, name: `使用者 ${userId}` });
}, 1000);
}, [userId]); // 當 userId 改變時重新獲取
// Effect 2: 獲取使用者的文章
useEffect(() => {
if (userId) {
console.log('獲取使用者文章 for userId:', userId);
// 模擬 API 呼叫
setTimeout(() => {
setPosts([`文章 1 by ${userId}`, `文章 2 by ${userId}`]);
}, 1500);
}
}, [userId]); // 當 userId 改變時重新獲取
// Effect 3: 訂閱線上狀態
useEffect(() => {
console.log('訂閱線上狀態');
const subscription = subscribeToOnlineStatus(setOnlineStatus);
return () => {
console.log('取消訂閱線上狀態');
subscription.unsubscribe();
};
}, []); // 只訂閱一次
return (
<div>
<h2>使用者儀表板</h2>
{user ? (
<div>
<p>使用者: {user.name} {onlineStatus ? '(線上)' : '(離線)'}</p>
<h3>文章列表:</h3>
<ul>
{posts.map((post, index) => (
<li key={index}>{post}</li>
))}
</ul>
</div>
) : (
<p>載入中...</p>
)}
</div>
);
}
// 模擬線上狀態訂閱
function subscribeToOnlineStatus(callback) {
const interval = setInterval(() => {
callback(Math.random() > 0.5); // 隨機線上/離線狀態
}, 3000);
return {
unsubscribe: () => clearInterval(interval)
};
}
export default UserDashboard;
6.7 常見的 useEffect
使用模式
1. 資料獲取 (Data Fetching)
import React, { useState, useEffect } from 'react';
function PostList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchPosts() {
try {
setLoading(true);
const response = await fetch('/api/posts');
if (!response.ok) {
throw new Error('Failed to fetch posts');
}
const data = await response.json();
setPosts(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchPosts();
}, []); // 只在元件掛載時獲取一次
if (loading) return <div>載入中...</div>;
if (error) return <div>錯誤: {error}</div>;
return (
<div>
<h2>文章列表</h2>
{posts.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
))}
</div>
);
}
export default PostList;
2. 響應式搜尋 (Debounced Search)
import React, { useState, useEffect } from 'react';
function Search() {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!searchTerm) {
setResults([]);
return;
}
setLoading(true);
// 使用 setTimeout 實現防抖 (debounce)
const timeoutId = setTimeout(async () => {
try {
// 模擬 API 呼叫
const response = await fetch(`/api/search?q=${searchTerm}`);
const data = await response.json();
setResults(data);
} catch (error) {
console.error('搜尋失敗:', error);
} finally {
setLoading(false);
}
}, 500); // 500ms 延遲
// 清理函數:如果 searchTerm 在 500ms 內再次改變,清除之前的計時器
return () => {
clearTimeout(timeoutId);
setLoading(false);
};
}, [searchTerm]);
return (
<div>
<input
type="text"
placeholder="搜尋..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
{loading && <p>搜尋中...</p>}
<ul>
{results.map((result, index) => (
<li key={index}>{result}</li>
))}
</ul>
</div>
);
}
export default Search;
6.8 useEffect
與類別元件生命週期的對應
對於熟悉類別元件的開發者,以下是 useEffect
與類別元件生命週期方法的對應關係:
類別元件生命週期 | useEffect 等價寫法 |
---|---|
componentDidMount |
useEffect(() => { ... }, []) |
componentDidUpdate |
useEffect(() => { ... }) |
componentWillUnmount |
useEffect(() => { return () => { ... } }, []) 中的清理函數 |
componentDidMount + componentDidUpdate |
useEffect(() => { ... }, [dependency]) |
類別元件範例:
class UserProfile extends React.Component {
constructor(props) {
super(props);
this.state = { user: null };
}
componentDidMount() {
this.fetchUser(this.props.userId);
}
componentDidUpdate(prevProps) {
if (prevProps.userId !== this.props.userId) {
this.fetchUser(this.props.userId);
}
}
componentWillUnmount() {
// 清理操作
}
fetchUser(userId) {
// 獲取使用者資料
}
render() {
return <div>{/* 渲染邏輯 */}</div>;
}
}
等價的函式元件與 useEffect
:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
async function fetchUser() {
// 獲取使用者資料
const userData = await api.getUser(userId);
setUser(userData);
}
fetchUser();
// 可選的清理函數
return () => {
// 清理操作
};
}, [userId]); // 當 userId 改變時重新執行
return <div>{/* 渲染邏輯 */}</div>;
}
6.9 useEffect
的最佳實踐
總是包含依賴項:如果你在 effect 中使用了元件作用域內的變數(state, props, 或由它們衍生的值),請將它們加入依賴陣列。
分離關注點:將不相關的邏輯分到不同的
useEffect
中。避免無限迴圈:確保依賴陣列正確,避免 effect 無限重複執行。
適當使用清理函數:對於訂閱、計時器、手動 DOM 操作等,總是記得清理。
條件執行:如果 effect 只在特定條件下執行,可以在 effect 內部加入條件判斷。
useEffect(() => {
if (shouldFetch) {
fetchData();
}
}, [shouldFetch]);
6.10 總結
useEffect
是現代 React 中處理副作用的主要方式。
useEffect
讓我們在函式元件中執行副作用操作。- 依賴陣列控制 effect 的執行時機。
- 清理函數用於清理資源,避免記憶體洩漏。
- 一個元件可以有多個
useEffect
,用於分離不同的關注點。 useEffect
可以模擬類別元件的所有生命週期方法。
掌握 useEffect
是使用現代 React 開發應用程式的關鍵技能。在下一章中,我們將學習其他重要的 Hook,如 useContext
、useReducer
等。