副作用處理的常見情境設計技巧
Fetch 請求伺服器端 API
情境說明
在 React 中,向伺服器端 API 發送請求(例如 GET、POST)是最常見的副作用之一。這種操作通常需要在元件載入時或特定條件改變時執行,例如取得資料、送出表單等。React 的 useEffect Hook 是處理這類副作用的最佳工具。
設計技巧
- 
使用 useEffect 管理 API 請求:將 API 請求放在 useEffect 中,確保它在元件渲染後執行。 
- 
處理載入狀態和錯誤:使用狀態 (useState) 管理資料的載入狀態、錯誤訊息和結果。 
- 
避免無限迴圈:在 useEffect 的依賴陣列中正確指定依賴項,防止不必要的重複請求。 
- 
清理機制:在元件卸載時取消未完成的請求(例如使用 AbortController)。 
- 
模組化 API 請求:將 API 請求邏輯抽離到獨立的函式,增加程式碼可維護性。 
完整程式碼範例
假設我們要從伺服器取得一組使用者清單並顯示在頁面上。
import React, { useState, useEffect } from "react";
// 模組化 API 請求函式
const fetchUsers = async (signal) => {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/users", {
      signal,
    });
    if (!response.ok) {
      throw new Error("無法取得使用者資料");
    }
    const data = await response.json();
    return data;
  } catch (error) {
    if (error.name === "AbortError") {
      console.log("請求已被取消");
      return [];
    }
    throw error;
  }
};
function UserList() {
  // 狀態管理:資料、載入狀態、錯誤訊息
  const [users, setUsers] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  useEffect(() => {
    // 建立 AbortController 用於取消請求
    const controller = new AbortController();
    const { signal } = controller;
    // 定義非同步函式
    const getUsers = async () => {
      setIsLoading(true);
      try {
        const data = await fetchUsers(signal);
        setUsers(data);
        setError(null);
      } catch (err) {
        setError(err.message);
      } finally {
        setIsLoading(false);
      }
    };
    // 執行請求
    getUsers();
    // 清理函式:元件卸載時取消請求
    return () => {
      controller.abort();
    };
  }, []); // 空依賴陣列,僅在元件首次渲染時執行
  // 渲染邏輯
  if (isLoading) return <div>載入中...</div>;
  if (error) return <div>錯誤:{error}</div>;
  return (
    <div>
      <h1>使用者清單</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}
export default UserList;
步驟分解
- 
安裝依賴:本範例使用原生 fetch,無需額外安裝套件。若使用 axios,需執行 npm install axios。 
- 
定義狀態:使用 useState 管理 users(資料)、isLoading(載入狀態)、error(錯誤訊息)。 
- 
撰寫 API 函式:將 fetch 邏輯抽離到 fetchUsers 函式,方便重複使用並處理錯誤。 
- 
在 useEffect 中執行:確保請求在元件渲染後執行,並使用 AbortController 防止記憶體洩漏。 
- 
處理 UI 狀態:根據 isLoading 和 error 顯示不同的 UI(載入中、錯誤、資料清單)。 
- 
清理請求:在 useEffect 的回傳函式中取消未完成的請求。 
控制外部套件
情境說明
在 React 中,許多外部套件(例如 Chart.js、Google Maps)需要初始化或在特定時機進行操作,這些都是副作用。這些操作通常需要在元件載入或更新時執行,且可能需要清理資源。
設計技巧
- 
使用 useEffect 初始化外部套件:在元件載入時初始化套件,並在更新時重新配置。 
- 
管理外部資源:確保在元件卸載時清理外部套件的資源(例如移除事件監聽器或銷毀實例)。 
- 
模組化邏輯:將外部套件的初始化邏輯抽離到獨立函式,方便維護。 
- 
處理依賴變化:根據依賴項動態更新外部套件的狀態。 
完整程式碼範例
假設我們使用 Chart.js 繪製一個簡單的柱狀圖,並在資料變化時更新圖表。
import React, { useEffect, useRef } from "react";
import Chart from "chart.js/auto";
function BarChart({ data }) {
  const chartRef = useRef(null); // 用於儲存 canvas 元素
  const chartInstanceRef = useRef(null); // 用於儲存 Chart.js 實例
  useEffect(() => {
    // 取得 canvas 的上下文
    const ctx = chartRef.current.getContext("2d");
    // 初始化 Chart.js
    chartInstanceRef.current = new Chart(ctx, {
      type: "bar",
      data: {
        labels: data.labels,
        datasets: [
          {
            label: "銷售量",
            data: data.values,
            backgroundColor: "rgba(75, 192, 192, 0.2)",
            borderColor: "rgba(75, 192, 192, 1)",
            borderWidth: 1,
          },
        ],
      },
      options: {
        scales: {
          y: {
            beginAtZero: true,
          },
        },
      },
    });
    // 清理函式:元件卸載時銷毀圖表
    return () => {
      if (chartInstanceRef.current) {
        chartInstanceRef.current.destroy();
      }
    };
  }, [data]); // 當 data 變化時重新渲染圖表
  return <canvas ref={chartRef} />;
}
function App() {
  const chartData = {
    labels: ["一月", "二月", "三月"],
    values: [10, 20, 30],
  };
  return (
    <div>
      <h1>銷售圖表</h1>
      <BarChart data={chartData} />
    </div>
  );
}
export default App;
步驟分解
- 
安裝 Chart.js:執行 npm install chart.js chart.js/auto。 
- 
建立 ref:使用 useRef 儲存 canvas 元素和 Chart.js 實例。 
- 
初始化圖表:在 useEffect 中創建 Chart.js 實例,並傳入資料和配置。 
- 
更新圖表:當 data 改變時,useEffect 會重新執行,更新圖表。 
- 
清理資源:在 useEffect 的回傳函式中銷毀 Chart.js 實例,防止記憶體洩漏。 
- 
傳遞資料:透過 props 傳入圖表資料,確保元件可重複使用。 
監聽或訂閱事件
情境說明
監聽瀏覽器事件(例如 resize、scroll)或訂閱外部事件(例如 WebSocket、第三方套件事件)是常見的副作用。這類操作需要在元件載入時綁定事件,並在卸載時移除監聽器。
設計技巧
- 
使用 useEffect 綁定事件:在元件載入時綁定事件監聽器。 
- 
清理事件監聽器:在元件卸載時移除監聽器,防止記憶體洩漏。 
- 
避免過多監聽:確保事件監聽器只綁定一次,並在依賴變化時正確更新。 
- 
模組化事件處理:將事件處理邏輯抽離到獨立函式,方便測試和維護。 
完整程式碼範例
假設我們要監聽視窗的 resize 事件,並顯示當前視窗寬度。
import React, { useState, useEffect } from "react";
function WindowSize() {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);
  useEffect(() => {
    // 事件處理函式
    const handleResize = () => {
      setWindowWidth(window.innerWidth);
    };
    // 綁定事件監聽器
    window.addEventListener("resize", handleResize);
    // 清理函式:移除事件監聽器
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []); // 空依賴陣列,僅在元件首次渲染時綁定
  return (
    <div>
      <h1>視窗寬度</h1>
      <p>當前寬度:{windowWidth}px</p>
    </div>
  );
}
export default WindowSize;
步驟分解
- 
定義狀態:使用 useState 儲存視窗寬度。 
- 
綁定事件:在 useEffect 中使用 window.addEventListener監聽 resize 事件。
- 
更新狀態:在事件處理函式中更新狀態,觸發重新渲染。 
- 
清理監聽器:在 useEffect 的回傳函式中移除事件監聽器。 
- 
渲染 UI:根據狀態顯示當前視窗寬度。 
不應該是副作用處理:使用者的操作所觸發的事情
情境說明
使用者的操作(例如點擊按鈕、輸入表單)通常應該直接在事件處理函式中處理,而不是在 useEffect 中。這些操作是同步的,且與元件的渲染週期無關,因此不應視為副作用。
設計技巧
- 
使用事件處理函式:將使用者的操作邏輯放在 onClick、onChange 等事件處理函式中。 
- 
避免在 useEffect 中處理:除非操作需要與外部系統(例如 API)互動,否則不要在 useEffect 中處理。 
- 
保持簡單:事件處理邏輯應該簡單直接,必要時抽離到獨立函式。 
- 
狀態管理:使用 useState 管理因使用者操作而改變的狀態。 
完整程式碼範例
假設我們有一個計數器,點擊按鈕時增加計數。
import React, { useState } from "react";
function Counter() {
  const [count, setCount] = useState(0);
  // 事件處理函式
  const handleIncrement = () => {
    setCount(count + 1);
  };
  return (
    <div>
      <h1>計數器</h1>
      <p>目前計數:{count}</p>
      <button onClick={handleIncrement}>增加</button>
    </div>
  );
}
export default Counter;
步驟分解
- 
定義狀態:使用 useState 儲存計數值。 
- 
處理事件:在 onClick 事件中直接更新狀態,無需 useEffect。 
- 
渲染 UI:根據狀態顯示計數值和按鈕。 
- 
保持簡單:邏輯集中在事件處理函式中,避免不必要的副作用。 
為什麼不使用 useEffect?
- 
使用者操作是即時的,直接更新狀態即可。 
- 
useEffect 適用於與渲染週期相關的副作用(例如 API 請求、事件監聽),不適用於同步的按鈕點擊邏輯。 
- 
在 useEffect 中處理點擊事件可能導致複雜的依賴管理或不必要的重新渲染。 
總結
- 
Fetch 請求伺服器端 API:使用 useEffect 管理非同步請求,處理載入狀態和錯誤,並使用 AbortController 清理。 
- 
控制外部套件:在 useEffect 中初始化和銷毀外部套件資源,確保依賴正確更新。 
- 
監聽或訂閱事件:在 useEffect 中綁定和移除事件監聽器,防止記憶體洩漏。 
- 
使用者的操作:直接在事件處理函式中處理,避免使用 useEffect。