隨著 UI 與業務邏輯越來越複雜,如果把所有 API 呼叫、狀態管理與事件監聽全都擠在 Component 裡面,組件很快就會變成難以維護的「巨型亂碼 (Spaghetti Code)」。Custom Hook 就是拯救組件可讀性與實現跨組件邏輯複用的終極武器!
Custom Hook 是一個名稱以 use 開頭的 JavaScript 函數,且其內部呼叫了其他 React 原生 Hooks(如 useState, useEffect, useRef 等)。
核心邏輯:
複用「狀態邏輯 (Stateful Logic)」,而非「狀態本身 (State Itself)」:呼叫同一個 Custom Hook 的兩個不同組件,彼此的 State 是完全獨立且互不干擾的。
遵循 Hook 規則:因為它本質上也是 Hook,因此內部同樣必須遵守「不能在條件判斷或迴圈中呼叫」的鐵則。
假設我們需要在多個頁面進行 API 資料讀取,原本的寫法會充滿重複的 useState 與 useEffect:
傳統寫法:邏輯與 UI 黏在一起
// 每個需要撈資料的組件都要重複寫這十幾行!
function UserProfile({ userId }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isCancelled = false;
setLoading(true);
fetchUser(userId)
.then(res => !isCancelled && setData(res))
.catch(err => !isCancelled && setError(err))
.finally(() => !isCancelled && setLoading(false));
return () => { isCancelled = true; };
}, [userId]);
if (loading) return <p>載入中...</p>;
if (error) return <p>發生錯誤:{error.message}</p>;
return <div>{data.name}</div>;
}
封裝為 Custom Hook:useFetch
// 將 API 請求邏輯徹底抽出
function useFetch(fetchFn, param) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isCancelled = false;
setLoading(true);
fetchFn(param)
.then((res) => !isCancelled && setData(res))
.catch((err) => !isCancelled && setError(err))
.finally(() => !isCancelled && setLoading(false));
return () => { isCancelled = true; };
}, [fetchFn, param]);
return { data, loading, error };
}
重構後的 Component
function UserProfile({ userId }) {
// 組件只關注「UI 如何呈現」
const { data, loading, error } = useFetch(fetchUser, userId);
if (loading) return <p>載入中...</p>;
if (error) return <p>發生錯誤:{error.message}</p>;
return <div>{data.name}</div>;
}
Custom Hook 不僅能處理 API,還能封裝各種瀏覽器行為(如網路狀態 useOnlineStatus、視窗尺寸 useWindowSize 或防手震 useDebounce):
import { useState, useEffect } from 'react';
// 封裝 Value 防手震邏輯
function useDebounce(value, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// 利用 Cleanup 清除未執行的 Timer
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
單一職責原則 (Single Responsibility):一個 Custom Hook 最好只專注解決一個問題(例如 useLocalStorage 專心處理本地儲存,不要把 API 請求也硬塞進去)。
語意化命名:務必以 use 為字首(如 useAuth, useForm),這樣 React ESLint Linter 才能正確為你進行 Hook 規則檢查。
回傳格式選擇:
回傳 物件 { a, b }:擴充性最佳,呼叫端可自由選用需要的屬性,或進行別名命名。
回傳 陣列 [a, b]:適合似 useState 的一對對應語意,讓呼叫端自由命名。