iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
JavaScript

React 觀念架構:從js 基礎到Hook 底層邏輯 系列 第 20 篇

Day 20 自訂 Hook (Custom Hooks) 封裝:抽離業務邏輯與邏輯複用

  • 分享至 

  • xImage
  •  

隨著 UI 與業務邏輯越來越複雜,如果把所有 API 呼叫、狀態管理與事件監聽全都擠在 Component 裡面,組件很快就會變成難以維護的「巨型亂碼 (Spaghetti Code)」。Custom Hook 就是拯救組件可讀性與實現跨組件邏輯複用的終極武器!

一、 什麼是 Custom Hook?

Custom Hook 是一個名稱以 use 開頭的 JavaScript 函數,且其內部呼叫了其他 React 原生 Hooks(如 useState, useEffect, useRef 等)。

核心邏輯:

  • 複用「狀態邏輯 (Stateful Logic)」,而非「狀態本身 (State Itself)」:呼叫同一個 Custom Hook 的兩個不同組件,彼此的 State 是完全獨立且互不干擾的。

  • 遵循 Hook 規則:因為它本質上也是 Hook,因此內部同樣必須遵守「不能在條件判斷或迴圈中呼叫」的鐵則。

二、 範例一:UI 與業務邏輯解耦 (useFetch)

假設我們需要在多個頁面進行 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>;
}

三、 範例二:DOM 事件與防手震封裝 (useDebounce)

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;
}

四、 設計 Custom Hook 的三大核心

  1. 單一職責原則 (Single Responsibility):一個 Custom Hook 最好只專注解決一個問題(例如 useLocalStorage 專心處理本地儲存,不要把 API 請求也硬塞進去)。

  2. 語意化命名:務必以 use 為字首(如 useAuth, useForm),這樣 React ESLint Linter 才能正確為你進行 Hook 規則檢查。

  3. 回傳格式選擇:

  • 回傳 物件 { a, b }:擴充性最佳,呼叫端可自由選用需要的屬性,或進行別名命名。

  • 回傳 陣列 [a, b]:適合似 useState 的一對對應語意,讓呼叫端自由命名。


上一篇
Day 19 useRef 雙重用途:DOM 操作與跨 Render 狀態快照
系列文
React 觀念架構:從js 基礎到Hook 底層邏輯 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言