【Vibe Coder 的 React 教學】#04 useEffect 與生命週期:處理副作用

測驗:useEffect 與生命週期:處理副作用

共 5 題,點選答案後會立即顯示結果

1. 在 React 中,以下哪一個屬於「副作用(Side Effect)」?

  • A. 回傳 JSX 元素
  • B. 根據 state 改變顯示的文字
  • C. 呼叫 API 抓取資料
  • D. 在 JSX 中顯示按鈕

2. useEffect 的第二個參數(依賴陣列)設為空陣列 [] 時,useEffect 會在什麼時候執行?

  • A. 每次組件重新渲染後都會執行
  • B. 只在組件第一次出現時執行一次
  • C. 組件消失時才執行
  • D. 永遠不會執行

3. 請看以下程式碼,當 userId 從 1 變成 2 時,useEffect 會重新執行嗎?

useEffect(() => { fetch(`/api/users/${userId}`); }, [userId]);
  • A. 會,因為 userId 在依賴陣列中
  • B. 不會,useEffect 只執行一次
  • C. 會,但必須手動觸發
  • D. 不會,fetch 不是副作用

4. 在 useEffect 中使用 setInterval 設定計時器時,為什麼需要回傳清理函數?

  • A. 為了讓計時器跑得更快
  • B. 組件消失時清除計時器,避免記憶體洩漏
  • C. React 規定必須回傳函數
  • D. 為了讓 useEffect 能夠執行

5. 為什麼 useEffect 不能直接宣告為 async 函式?

// 這是錯誤的寫法: useEffect(async () => { const data = await fetch(‘/api/data’); }, []);
  • A. async 函式執行速度太慢
  • B. React 不支援 async/await 語法
  • C. useEffect 的回傳值有特殊用途(清理函數),不能是 Promise
  • D. fetch 不能在 useEffect 中使用

一句話說明

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)

這段代碼做了什麼

  1. 每次按按鈕,seconds 加 1
  2. 每次畫面更新後,自動更新網頁標題
  3. 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 題,包含情境題與錯誤診斷題。

1. 你正在開發一個使用者個人頁面,需要在組件載入時從 API 抓取使用者資料,而且當 URL 中的 userId 參數改變時要重新抓取。以下哪個 useEffect 寫法最正確? 情境題

  • A. useEffect(() => { fetchUser(userId); });
  • B. useEffect(() => { fetchUser(userId); }, []);
  • C. useEffect(() => { fetchUser(userId); }, [userId]);
  • D. useEffect(async () => { await fetchUser(userId); }, [userId]);

2. 你需要實作一個即時通知功能,使用 WebSocket 訂閱伺服器的訊息。組件消失時應該取消訂閱。以下哪個實作方式最佳? 情境題

  • A. 在 useEffect 外面訂閱,不需要取消
  • B. 在 useEffect 中訂閱,並在 return 的清理函數中取消訂閱
  • C. 在 useEffect 中訂閱,使用另一個 useEffect 取消訂閱
  • D. 直接在組件函式中訂閱,不使用 useEffect

3. 你的搜尋功能需要在使用者輸入搜尋關鍵字後自動查詢 API。但你發現每打一個字就會發送一次請求,造成效能問題。要如何在 useEffect 中只對「有內容的搜尋詞」發送請求? 情境題

useEffect(() => { // 該怎麼寫? }, [searchTerm]);
  • A. 在 useEffect 內加上 if (searchTerm) 條件判斷後再發送請求
  • B. 移除依賴陣列中的 searchTerm
  • C. 把依賴陣列改成空陣列 []
  • D. 不使用 useEffect,改用 onClick 事件

4. 小美寫了以下程式碼,但發現瀏覽器變得很慢,Console 一直印出訊息。問題出在哪裡? 錯誤診斷

function Counter() { const [count, setCount] = useState(0); useEffect(() => { setCount(count + 1); console.log(‘count changed’); }); return <div>{count}</div>; }
  • A. useState 的初始值應該是字串
  • B. useEffect 沒有依賴陣列,每次渲染都會執行 setCount,造成無限迴圈
  • C. console.log 會阻塞渲染
  • D. setCount 應該放在 return 之後

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>; }
  • A. fetch 語法有錯誤
  • B. userId 沒有放在依賴陣列中
  • C. API 回應還沒回來時組件就消失了,但 setUser 仍然被呼叫
  • D. useState 的初始值不能是 null

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *