Skip to main content

useEffect

主要用來處理副作用(side effects),這些副作用包括但不限於 非同步請求(如 API 呼叫)、訂閱(subscription)、DOM 操作 等。但 useEffect 本身不會強制要求非同步功能,你可以在其中執行同步或非同步的操作。

常見適用於 useEffect 的情境

  1. 非同步請求(API 呼叫)

  2. 監聽視窗大小變化、滾動事件

  3. 設定或清除計時器(setTimeoutsetInterval

  4. 訂閱 WebSocket 或事件監聽

  5. 修改 document.titlelocalStorage

錯誤用法:直接將 useEffect 設為 async

雖然 useEffect 經常用來執行非同步操作,但它本身不能直接標記為 async,因為 useEffect 期望回傳 undefinedcleanup function,但 async function 會回傳 Promise,導致 React 產生錯誤。

useEffect(async () => {  // ❌ 錯誤:useEffect 不能是 async
const data = await fetchData();
setState(data);
}, []);

正確使用方式

如果 useEffect 內部需要執行非同步函式,應該在 useEffect 內部定義一個 async 函式,然後執行它

import { useEffect, useState } from "react";

function MyComponent() {
const [data, setData] = useState(null);

useEffect(() => {
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
const result = await response.json();
setData(result);
} catch (error) {
console.error("Fetch error:", error);
}
}

fetchData(); // ✅ 在 useEffect 內部執行 async 函式
}, []);

return <div>{data ? JSON.stringify(data) : "Loading..."}</div>;
}

補充:如何正確處理 useEffect 清除(Cleanup Function)

如果 useEffect 內有訂閱或計時器,記得在 return 內回傳清理函式來避免記憶體洩漏。

監聽事件並清除

useEffect(() => {
const handleResize = () => console.log("Window resized");

window.addEventListener("resize", handleResize);

return () => {
window.removeEventListener("resize", handleResize); // ✅ 清除監聽事件
};
}, []);

WebSocket 訂閱

useEffect(() => {
const socket = new WebSocket("wss://example.com");

socket.onmessage = (event) => console.log("Received:", event.data);

return () => {
socket.close(); // ✅ 清除 WebSocket
};
}, []);

結論

useEffect 不一定要執行非同步功能,但很適合處理副作用(像是 API 請求、事件監聽等)。
不能直接讓 useEffect 變成 async,而是要在內部定義 async function 來執行非同步操作。
如果有訂閱或計時器,一定要在 return 內清除它,以避免記憶體洩漏。

副作用的實際案例

在 React 中,副作用(Side Effects) 是指元件渲染過程中會影響外部環境的行為。例如:

  • 發送 API 請求

  • 操作瀏覽器 DOM

  • 設定或清除計時器

  • 監聽與移除事件

  • 訂閱 WebSocket

這些行為都不是純粹的 UI 渲染,而是額外影響程式的外部狀態,因此稱為 副作用。React 透過 useEffect 來管理這些副作用,確保它們在適當的時機執行和清理。


1. API 請求:載入資料

最常見的副作用之一是從 API 取得資料,然後更新 state 來顯示資料。

需求

當元件掛載時,從 API 取得資料,並在畫面上顯示。

實作

import { useEffect, useState } from "react";

function FetchDataComponent() {
const [data, setData] = useState(null);

useEffect(() => {
async function fetchData() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
const result = await response.json();
setData(result);
} catch (error) {
console.error("API 錯誤:", error);
}
}

fetchData();
}, []); // 只有元件掛載時執行

return (
<div>
<h2>API 資料</h2>
{data ? <pre>{JSON.stringify(data, null, 2)}</pre> : "載入中..."}
</div>
);
}

export default FetchDataComponent;

剖析

  • useEffect(() => { fetchData(); }, [])

    • fetchData 是一個 async 函式,發送 API 請求並更新 state

    • [] 代表只有元件掛載時執行一次,不會重複執行。


2. 事件監聽(Event Listener)

有時候我們需要監聽視窗大小變化,並根據變化來更新 state

需求

當使用者調整視窗大小時,顯示目前的寬度。

實作

import { useEffect, useState } from "react";

function WindowResizeComponent() {
const [width, setWidth] = useState(window.innerWidth);

useEffect(() => {
const handleResize = () => {
setWidth(window.innerWidth);
};

window.addEventListener("resize", handleResize);

return () => {
window.removeEventListener("resize", handleResize); // 🧹 清除監聽
};
}, []);

return (
<div>
<h2>視窗寬度:{width}px</h2>
</div>
);
}

export default WindowResizeComponent;

剖析

  • window.addEventListener("resize", handleResize);

    • 監聽視窗大小變化,當 resize 事件發生時,更新 width
  • return () => { window.removeEventListener("resize", handleResize); }

    • 重要! React 會重新執行 useEffect(例如 setState 觸發重新渲染),所以在元件卸載時,React 會清除這個監聽,避免記憶體洩漏。

3. 計時器(setInterval / setTimeout)

我們可能需要建立計時器,例如顯示當前時間。

需求

顯示目前時間,每秒更新一次。

實作

import { useEffect, useState } from "react";

function ClockComponent() {
const [time, setTime] = useState(new Date().toLocaleTimeString());

useEffect(() => {
const interval = setInterval(() => {
setTime(new Date().toLocaleTimeString());
}, 1000);

return () => clearInterval(interval); // 🧹 清除計時器
}, []);

return <h2>現在時間:{time}</h2>;
}

export default ClockComponent;

剖析

  • setInterval 每秒更新 time

  • return () => clearInterval(interval);

    • 確保元件卸載時,清除計時器,避免記憶體洩漏。

4. WebSocket 連線

即時應用(如聊天室)通常會透過 WebSocket 來即時更新資料。

需求

當元件掛載時,建立 WebSocket 連線,接收訊息,並在卸載時關閉連線。

實作

import { useEffect, useState } from "react";

function WebSocketComponent() {
const [message, setMessage] = useState("");

useEffect(() => {
const socket = new WebSocket("wss://example.com/socket");

socket.onmessage = (event) => {
setMessage(event.data);
};

return () => {
socket.close(); // 🧹 清除 WebSocket 連線
};
}, []);

return <h2>WebSocket 訊息:{message}</h2>;
}

export default WebSocketComponent;

剖析

  • const socket = new WebSocket("wss://example.com/socket");

    • 建立 WebSocket 連線。
  • socket.onmessage = (event) => { setMessage(event.data); }

    • 當接收到新訊息時,更新 message
  • return () => { socket.close(); }

    • 確保元件卸載時,關閉 WebSocket 連線,避免資源浪費。

5. 操作 document.title

有時我們想要根據頁面狀態來更新標題,例如顯示「未讀訊息數」。

需求

count 更新時,變更網頁標題。

實作

import { useEffect, useState } from "react";

function TitleComponent() {
const [count, setCount] = useState(0);

useEffect(() => {
document.title = `未讀訊息數:${count}`;
}, [count]); // 只有當 count 變更時執行

return (
<div>
<h2>未讀訊息數:{count}</h2>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}

export default TitleComponent;

剖析

  • document.title = "未讀訊息數:" + count;

    • 修改網頁標題來反映未讀訊息數。
  • useEffect 依賴 count,只有 count 更新時才會執行


總結

副作用類型使用情境清理方式
API 請求取得遠端資料無需清理,但可取消請求
事件監聽resizescrollremoveEventListener
計時器setTimeoutsetIntervalclearTimeoutclearInterval
WebSocket即時通訊socket.close()
修改 DOMdocument.titlelocalStorage無需清理

重點

  • useEffect(() => {...}, []) → 只執行一次

  • useEffect(() => {...}, [state]) → 當 state 變更時執行

  • 務必清除副作用(監聽事件、計時器、WebSocket)