副作用處理的常見情境設計技巧
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。