測驗:useEffect 與生命週期:處理副作用
共 5 題,點選答案後會立即顯示結果
1. 在 React 中,以下哪一個屬於「副作用(Side Effect)」?
2. useEffect 的第二個參數(依賴陣列)設為空陣列 [] 時,useEffect 會在什麼時候執行?
3. 請看以下程式碼,當 userId 從 1 變成 2 時,useEffect 會重新執行嗎?
useEffect(() => {
fetch(`/api/users/${userId}`);
}, [userId]);
4. 在 useEffect 中使用 setInterval 設定計時器時,為什麼需要回傳清理函數?
5. 為什麼 useEffect 不能直接宣告為 async 函式?
// 這是錯誤的寫法:
useEffect(async () => {
const data = await fetch(‘/api/data’);
}, []);
一句話說明
useEffect 讓你在「畫面畫好之後」執行額外操作,像是抓 API 資料、設定計時器。
前置知識
- 已讀過第 3 篇:了解 Props 與 State
- 知道 useState 怎麼用
什麼是副作用(Side Effect)?
在 React 裡,「渲染」是指把 JSX 變成畫面。但有些操作不是「畫畫面」:
| 主要作用(渲染) | 副作用(Side Effect) |
|---|---|
| 回傳 JSX | 呼叫 API 抓資料 |
| 顯示文字、按鈕 | 設定計時器 |
| 根據 state 改變畫面 | 訂閱事件(如 WebSocket) |
| 操作 DOM(如設定 title) |
一句話:副作用是「畫面以外」的操作。
最小範例
import { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
document.title = `計時:${seconds} 秒`; // 這是副作用
});
return <button onClick={() => setSeconds(seconds + 1)}>
{seconds} 秒
</button>;
}
Code language: JavaScript (javascript)這段代碼做了什麼:
- 每次按按鈕,seconds 加 1
- 每次畫面更新後,自動更新網頁標題
- useEffect 裡的程式在「畫面畫好之後」執行
逐行翻譯
useEffect(() => { // 「當畫面更新後,執行這個函式」
document.title = `...`; // 執行副作用(改標題)
}); // 沒有第二個參數 = 每次更新都執行
Code language: JavaScript (javascript)依賴陣列:控制何時執行
useEffect 的第二個參數是「依賴陣列」,決定什麼時候重新執行:
情況 1:沒有依賴陣列
useEffect(() => {
console.log('每次畫面更新都會執行');
});
Code language: JavaScript (javascript)翻譯:「每次組件重新渲染後都執行」
情況 2:空陣列 []
useEffect(() => {
console.log('只在組件出現時執行一次');
}, []); // 注意這個空陣列
Code language: JavaScript (javascript)翻譯:「只在組件第一次出現時執行,之後不管」
情況 3:有依賴
useEffect(() => {
console.log(`userId 變了:${userId}`);
}, [userId]); // 監聽 userId
Code language: JavaScript (javascript)翻譯:「當 userId 變化時才執行」
對照表
| 依賴陣列 | 執行時機 | 常見用途 |
|---|---|---|
| 沒有 | 每次渲染後 | 少用,容易出問題 |
[] |
只有第一次 | 抓初始資料、設定訂閱 |
[a, b] |
a 或 b 變化時 | 根據參數變化抓資料 |
實作:從 API 抓取資料
這是 AI 最常幫你寫的 useEffect 用法:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchUser() {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
setLoading(false);
}
fetchUser();
}, [userId]); // userId 變化時重新抓
if (loading) return <div>載入中...</div>;
return <div>{user.name}</div>;
}
Code language: JavaScript (javascript)逐步解讀:
useEffect(() => { // 畫面更新後執行
async function fetchUser() { // 在裡面定義 async 函式
setLoading(true); // 開始載入
const response = await fetch(...); // 呼叫 API
const data = await response.json(); // 解析回應
setUser(data); // 更新狀態
setLoading(false); // 載入完成
}
fetchUser(); // 執行這個函式
}, [userId]); // 當 userId 變化時重新執行
Code language: JavaScript (javascript)為什麼要在 useEffect 裡面定義函式? 因為 useEffect 的回傳值有特殊用途(清理函數),不能直接是 Promise。
清理函數(Cleanup)
有些副作用需要「收尾」,否則會造成記憶體洩漏:
useEffect(() => {
// 設定計時器
const timer = setInterval(() => {
console.log('tick');
}, 1000);
// 回傳清理函數
return () => {
clearInterval(timer); // 組件消失時清除計時器
};
}, []);
Code language: JavaScript (javascript)翻譯:
useEffect(() => {
// 「組件出現時」做這些事
const timer = setInterval(...);
return () => {
// 「組件消失前」做這些事(清理)
};
}, []);
Code language: JavaScript (javascript)什麼時候需要清理?
| 需要清理 | 不需要清理 |
|---|---|
| setInterval / setTimeout | 呼叫 API(fetch) |
| 訂閱 WebSocket | 修改 document.title |
| 添加 event listener | console.log |
| 第三方函式庫初始化 | 更新 state |
常見錯誤與解法
錯誤 1:無限迴圈
// 錯誤:會無限執行
useEffect(() => {
setCount(count + 1); // 改 state → 重新渲染 → 又執行 useEffect
});
// 正確:加上空陣列
useEffect(() => {
setCount(count + 1);
}, []); // 只執行一次
Code language: JavaScript (javascript)錯誤 2:忘記加依賴
// 錯誤:userId 變了但不會重新抓資料
useEffect(() => {
fetch(`/api/users/${userId}`);
}, []); // 空陣列只執行一次
// 正確:加上 userId
useEffect(() => {
fetch(`/api/users/${userId}`);
}, [userId]); // userId 變化時重新執行
Code language: JavaScript (javascript)錯誤 3:直接用 async
// 錯誤:useEffect 不能直接是 async 函式
useEffect(async () => {
const data = await fetch(...);
}, []);
// 正確:在裡面定義 async 函式
useEffect(() => {
async function fetchData() {
const data = await fetch(...);
}
fetchData();
}, []);
Code language: JavaScript (javascript)AI 常用的 useEffect 模式
模式 1:載入初始資料
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => setData(data));
}, []);
Code language: JavaScript (javascript)模式 2:監聽參數變化
useEffect(() => {
if (searchTerm) {
fetch(`/api/search?q=${searchTerm}`)
.then(res => res.json())
.then(setResults);
}
}, [searchTerm]);
Code language: JavaScript (javascript)模式 3:訂閱與取消訂閱
useEffect(() => {
const subscription = eventSource.subscribe(handleEvent);
return () => subscription.unsubscribe();
}, []);
Code language: JavaScript (javascript)Vibe Coder 檢查點
看到 useEffect 時確認:
- [ ] 有依賴陣列嗎?沒有可能會無限執行
- [ ] 依賴陣列有漏掉的變數嗎?
- [ ] 需要清理嗎?(計時器、訂閱、事件監聽)
- [ ] 有處理 loading 狀態嗎?
- [ ] 有處理錯誤嗎?
Vibe Coding 技巧:請 AI 幫忙除錯 useEffect
當 useEffect 出問題時,可以這樣問 AI:
無限迴圈時:
我的 useEffect 一直重複執行,這是我的代碼:
[貼上代碼]
請幫我檢查依賴陣列有沒有問題
Code language: CSS (css)資料沒更新時:
userId 變了但畫面沒更新,這是我的 useEffect:
[貼上代碼]
是不是依賴陣列的問題?
Code language: CSS (css)記憶體洩漏警告時:
React 說 "Can't perform a state update on an unmounted component"
這是我的代碼:
[貼上代碼]
需要加清理函數嗎?
Code language: JavaScript (javascript)重點整理
| 概念 | 一句話 |
|---|---|
| 副作用 | 渲染以外的操作(API、計時器、DOM) |
| useEffect | 在「畫面畫好後」執行副作用 |
| 依賴陣列 | 控制何時重新執行 |
| 空陣列 [] | 只執行一次 |
| 清理函數 | 組件消失前的收尾工作 |
下一篇預告
下一篇我們會學習「條件渲染與列表」,看 AI 怎麼用 map 和三元運算子產生動態畫面。
進階測驗:useEffect 與生命週期:處理副作用
測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。
共 5 題,包含情境題與錯誤診斷題。
1. 你正在開發一個使用者個人頁面,需要在組件載入時從 API 抓取使用者資料,而且當 URL 中的 userId 參數改變時要重新抓取。以下哪個 useEffect 寫法最正確? 情境題
2. 你需要實作一個即時通知功能,使用 WebSocket 訂閱伺服器的訊息。組件消失時應該取消訂閱。以下哪個實作方式最佳? 情境題
3. 你的搜尋功能需要在使用者輸入搜尋關鍵字後自動查詢 API。但你發現每打一個字就會發送一次請求,造成效能問題。要如何在 useEffect 中只對「有內容的搜尋詞」發送請求? 情境題
useEffect(() => {
// 該怎麼寫?
}, [searchTerm]);
4. 小美寫了以下程式碼,但發現瀏覽器變得很慢,Console 一直印出訊息。問題出在哪裡? 錯誤診斷
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
console.log(‘count changed’);
});
return <div>{count}</div>;
}
5. 小明收到 React 警告:「Can’t perform a state update on an unmounted component」。他的程式碼如下,問題最可能是什麼? 錯誤診斷
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
return <div>{user?.name}</div>;
}