Skip to main content

副作用處理的常見情境設計技巧

Fetch 請求伺服器端 API

情境說明

在 React 中,向伺服器端 API 發送請求(例如 GET、POST)是最常見的副作用之一。這種操作通常需要在元件載入時或特定條件改變時執行,例如取得資料、送出表單等。React 的 useEffect Hook 是處理這類副作用的最佳工具。

設計技巧

  1. 使用 useEffect 管理 API 請求:將 API 請求放在 useEffect 中,確保它在元件渲染後執行。

  2. 處理載入狀態和錯誤:使用狀態 (useState) 管理資料的載入狀態、錯誤訊息和結果。

  3. 避免無限迴圈:在 useEffect 的依賴陣列中正確指定依賴項,防止不必要的重複請求。

  4. 清理機制:在元件卸載時取消未完成的請求(例如使用 AbortController)。

  5. 模組化 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;

步驟分解

  1. 安裝依賴:本範例使用原生 fetch,無需額外安裝套件。若使用 axios,需執行 npm install axios。

  2. 定義狀態:使用 useState 管理 users(資料)、isLoading(載入狀態)、error(錯誤訊息)。

  3. 撰寫 API 函式:將 fetch 邏輯抽離到 fetchUsers 函式,方便重複使用並處理錯誤。

  4. 在 useEffect 中執行:確保請求在元件渲染後執行,並使用 AbortController 防止記憶體洩漏。

  5. 處理 UI 狀態:根據 isLoading 和 error 顯示不同的 UI(載入中、錯誤、資料清單)。

  6. 清理請求:在 useEffect 的回傳函式中取消未完成的請求。


控制外部套件

情境說明

在 React 中,許多外部套件(例如 Chart.js、Google Maps)需要初始化或在特定時機進行操作,這些都是副作用。這些操作通常需要在元件載入或更新時執行,且可能需要清理資源。

設計技巧

  1. 使用 useEffect 初始化外部套件:在元件載入時初始化套件,並在更新時重新配置。

  2. 管理外部資源:確保在元件卸載時清理外部套件的資源(例如移除事件監聽器或銷毀實例)。

  3. 模組化邏輯:將外部套件的初始化邏輯抽離到獨立函式,方便維護。

  4. 處理依賴變化:根據依賴項動態更新外部套件的狀態。

完整程式碼範例

假設我們使用 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;

步驟分解

  1. 安裝 Chart.js:執行 npm install chart.js chart.js/auto。

  2. 建立 ref:使用 useRef 儲存 canvas 元素和 Chart.js 實例。

  3. 初始化圖表:在 useEffect 中創建 Chart.js 實例,並傳入資料和配置。

  4. 更新圖表:當 data 改變時,useEffect 會重新執行,更新圖表。

  5. 清理資源:在 useEffect 的回傳函式中銷毀 Chart.js 實例,防止記憶體洩漏。

  6. 傳遞資料:透過 props 傳入圖表資料,確保元件可重複使用。


監聽或訂閱事件

情境說明

監聽瀏覽器事件(例如 resize、scroll)或訂閱外部事件(例如 WebSocket、第三方套件事件)是常見的副作用。這類操作需要在元件載入時綁定事件,並在卸載時移除監聽器。

設計技巧

  1. 使用 useEffect 綁定事件:在元件載入時綁定事件監聽器。

  2. 清理事件監聽器:在元件卸載時移除監聽器,防止記憶體洩漏。

  3. 避免過多監聽:確保事件監聽器只綁定一次,並在依賴變化時正確更新。

  4. 模組化事件處理:將事件處理邏輯抽離到獨立函式,方便測試和維護。

完整程式碼範例

假設我們要監聽視窗的 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;

步驟分解

  1. 定義狀態:使用 useState 儲存視窗寬度。

  2. 綁定事件:在 useEffect 中使用 window.addEventListener 監聽 resize 事件。

  3. 更新狀態:在事件處理函式中更新狀態,觸發重新渲染。

  4. 清理監聽器:在 useEffect 的回傳函式中移除事件監聽器。

  5. 渲染 UI:根據狀態顯示當前視窗寬度。


不應該是副作用處理:使用者的操作所觸發的事情

情境說明

使用者的操作(例如點擊按鈕、輸入表單)通常應該直接在事件處理函式中處理,而不是在 useEffect 中。這些操作是同步的,且與元件的渲染週期無關,因此不應視為副作用。

設計技巧

  1. 使用事件處理函式:將使用者的操作邏輯放在 onClick、onChange 等事件處理函式中。

  2. 避免在 useEffect 中處理:除非操作需要與外部系統(例如 API)互動,否則不要在 useEffect 中處理。

  3. 保持簡單:事件處理邏輯應該簡單直接,必要時抽離到獨立函式。

  4. 狀態管理:使用 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;

步驟分解

  1. 定義狀態:使用 useState 儲存計數值。

  2. 處理事件:在 onClick 事件中直接更新狀態,無需 useEffect。

  3. 渲染 UI:根據狀態顯示計數值和按鈕。

  4. 保持簡單:邏輯集中在事件處理函式中,避免不必要的副作用。

為什麼不使用 useEffect?

  • 使用者操作是即時的,直接更新狀態即可。

  • useEffect 適用於與渲染週期相關的副作用(例如 API 請求、事件監聽),不適用於同步的按鈕點擊邏輯。

  • 在 useEffect 中處理點擊事件可能導致複雜的依賴管理或不必要的重新渲染。


總結

  • Fetch 請求伺服器端 API:使用 useEffect 管理非同步請求,處理載入狀態和錯誤,並使用 AbortController 清理。

  • 控制外部套件:在 useEffect 中初始化和銷毀外部套件資源,確保依賴正確更新。

  • 監聽或訂閱事件:在 useEffect 中綁定和移除事件監聽器,防止記憶體洩漏。

  • 使用者的操作:直接在事件處理函式中處理,避免使用 useEffect。