Skip to main content

React 中的副作用處理:effect 初探

什麼是 Effect

在 React 中,副作用 (Side Effect) 指的是在渲染元件 (Component) 時,執行一些與渲染無直接關係的操作。例如:

  • 從伺服器獲取資料 (如 API 請求)

  • 操作 DOM (如修改頁面標題)

  • 設定計時器 (如 setTimeout 或 setInterval)

  • 訂閱外部事件 (如 WebSocket 或事件監聽器)

這些操作不直接影響元件的渲染結果,但會影響應用程式的狀態或其他外部資源。React 提供了 useEffect Hook 來處理這些副作用,讓你可以在適當的時機執行它們。

簡單來說,Effect 就是 React 用來管理副作用的機制,確保副作用不會干擾元件的渲染流程。


React Component Function 中的副作用

在 React 的函數元件中,副作用通常不應該直接寫在元件的頂層,因為元件函數會在每次渲染時執行。如果直接在元件函數中執行副作用,可能導致以下問題:

  • 重複執行:每次渲染時,副作用會重複運行,可能造成資源浪費或錯誤。

  • 難以控制時機:無法精確控制副作用何時執行或清理。

不正確的副作用寫法

以下是一個錯誤的例子,直接在元件函數中執行副作用:

import React from "react";

function MyComponent() {
// 不正確:在元件頂層直接執行副作用
document.title = "正在載入..."; // 每次渲染都會執行
console.log("元件渲染了!");

return <div>Hello, World!</div>;
}

export default MyComponent;

問題

  • 每次元件渲染,document.title 都會被重新設定。

  • 如果有其他副作用(如 API 請求),可能會導致多次不必要的請求,影響效能。

解決方案:使用 useEffect Hook 來控制副作用的執行時機,避免在元件頂層直接執行。


useEffect 初探

useEffect 是 React 提供的一個 Hook,用來在函數元件中處理副作用。它允許你指定副作用的執行時機,並在需要時執行清理工作。

useEffect 基本語法

import { useEffect } from "react";

useEffect(() => {
// 副作用程式碼
}, [dependencies]);
  • 第一個參數:一個回呼函數,包含要執行的副作用程式碼。

  • 第二個參數:依賴陣列 (dependency array),控制副作用何時執行。如果陣列中的值改變,副作用會重新執行。

    • 如果依賴陣列為空 ([]),副作用只在元件首次渲染時執行一次。

    • 如果省略依賴陣列,副作用會在每次渲染時都執行。

範例:使用 useEffect 修改頁面標題

以下是一個簡單的 useEffect 範例,用來在元件渲染後修改頁面標題:

import React, { useEffect, useState } from "react";

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

// 使用 useEffect 處理副作用
useEffect(() => {
document.title = `你點擊了 ${count}`;
}, [count]); // 當 count 改變時,重新執行副作用

return (
<div>
<p>你點擊了 {count}</p>
<button onClick={() => setCount(count + 1)}>點我</button>
</div>
);
}

export default TitleUpdater;

說明

  • useEffect 確保 document.title 只在 count 改變時更新。

  • 依賴陣列 [count] 表示副作用依賴於 count 狀態,當 count 改變時,副作用會重新執行。

執行時機

  • useEffect 中的回呼函數在元件渲染完成後執行(即 DOM 更新後)。

  • 如果有依賴陣列,只有當陣列中的值改變時,副作用才會重新執行。


每次 Render 都有其自己版本的 Effect 函式

在 React 中,每次元件渲染都會產生一個新的 effect 函式。這意味著 useEffect 內的回呼函數會捕捉當前渲染時的 propsstate 值。這種行為確保了副作用的邏輯與當前渲染的狀態一致。

範例:展示 Effect 捕捉當前狀態

import React, { useEffect, useState } from "react";

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

useEffect(() => {
console.log(`當前 count 值: ${count}`);
}, [count]); // 每次 count 改變時,執行新的 effect

return (
<div>
<p>計數: {count}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
);
}

export default Counter;

說明

  • 每次 count 改變,元件重新渲染,產生一個新的 useEffect 回呼函數。

  • 新的回呼函數會捕捉當前的 count 值,並在控制台輸出。

  • 這確保了副作用始終使用最新的狀態值。

注意

  • 如果依賴陣列中遺漏了某些依賴(如 count),可能導致副作用使用舊的狀態值,造成邏輯錯誤。

  • 使用 ESLint 的 react-hooks/exhaustive-deps 規則可以幫助檢查依賴陣列是否完整。


每次 Render 都有其自己版本的 Cleanup 函式

useEffect 允許你返回一個 清理函數 (cleanup function),用來在副作用重新執行或元件卸載 (unmount) 前清理資源。每次渲染都會產生一個新的清理函數,對應當前的 effect 函數。

清理函數的用途

  • 取消訂閱(如 WebSocket 或事件監聽器)。

  • 清除計時器(如 setTimeout 或 setInterval)。

  • 避免記憶體洩漏。

清理函數的執行時機

  • 元件重新渲染:當依賴陣列中的值改變時,React 會先執行上一次的清理函數,再執行新的 effect 函數。

  • 元件卸載:當元件從 DOM 中移除時,React 會執行最後一次的清理函數。

範例:使用 Cleanup 函數清除計時器

以下是一個使用 useEffect 設定計時器並清理的範例:

import React, { useEffect, useState } from "react";

function Timer() {
const [seconds, setSeconds] = useState(0);

useEffect(() => {
// 設定計時器
const timer = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);

// 返回清理函數
return () => {
console.log("清理計時器");
clearInterval(timer); // 清除計時器
};
}, []); // 空依賴陣列,effect 只在首次渲染時執行

return (
<div>
<p>經過秒數: {seconds}</p>
</div>
);
}

export default Timer;

說明

  • setInterval 每秒更新 seconds 狀態。

  • useEffect 返回的清理函數在元件卸載時執行,清除計時器,防止記憶體洩漏。

  • 因為依賴陣列為空 [],effect 和清理函數只在元件掛載和卸載時各執行一次。

範例:依賴改變時的清理

如果依賴陣列不為空,每次依賴改變時,會先執行清理函數,再執行新的 effect:

import React, { useEffect, useState } from "react";

function UserProfile({ userId }) {
const [data, setData] = useState(null);

useEffect(() => {
console.log(`獲取 userId ${userId} 的資料`);
// 模擬 API 請求
const fetchData = async () => {
// 假設這是一個 API 請求
setData(`使用者 ${userId} 的資料`);
};
fetchData();

// 返回清理函數
return () => {
console.log(`清理 userId ${userId} 的資源`);
// 這裡可以取消 API 請求或清除其他資源
};
}, [userId]); // 當 userId 改變時,重新執行 effect

return <div>{data || "載入中..."}</div>;
}

export default UserProfile;

說明

  • 當 userId 改變時,React 會先執行上一次的清理函數(輸出清理訊息),再執行新的 effect(獲取新資料)。

  • 這確保了舊的資源被正確清理,避免重複請求或記憶體洩漏。


總結

  1. 什麼是 Effect:Effect 是 React 用來處理副作用的機制,像是 API 請求、DOM 操作等。

  2. 元件中的副作用:不應直接在元件頂層執行副作用,應使用 useEffect 控制時機。

  3. useEffect 初探:useEffect 是一個 Hook,用來在渲染後執行副作用,依賴陣列控制執行時機。

  4. 每次 Render 的 Effect:每次渲染產生新的 effect 函數,捕捉當前狀態和 props。

  5. 每次 Render 的 Cleanup:每次渲染產生新的清理函數,在依賴改變或元件卸載時執行,清理資源。

實作建議

  • 始終正確設定依賴陣列,避免邏輯錯誤。

  • 使用清理函數來防止記憶體洩漏,特別是涉及計時器或訂閱的情況。

  • 如果不確定依賴陣列是否正確,使用 ESLint 的 react-hooks 插件檢查。