自訂 Hook 是什麼?如何封裝自己的邏輯

更新日期: 2025 年 4 月 25 日

學會基本的 React Hook(如 useStateuseEffect)後,你可能會發現:

  • 有些邏輯會在好幾個元件中重複出現
  • 元件變得又長又難讀,因為邏輯與畫面混在一起
  • 每次寫類似的邏輯都要重新複製貼上

這時,你就需要 自訂 Hook(Custom Hook) 來幫你「抽出邏輯」、「共用邏輯」,讓程式碼乾淨又容易維護。


什麼是自訂 Hook?

簡單定義

在 React 中,Hook 是一種可以在函式元件中使用的特殊函式,像是 useStateuseEffect

自訂 Hook(Custom Hook),則是指:

🔁 「把重複使用的邏輯用函式包裝起來,讓不同元件可以共用。」

說白一點,它就像是一段你自己寫的工具程式,只不過它裡面可以使用其他 React Hook,而且必須以 use 開頭命名

這樣命名是有目的的:React 會透過函式名稱來判斷這是不是一個 Hook,進而正確追蹤狀態更新與副作用的順序。

如果你忘了加 use,React 可能會跳錯或無法正確執行。

👀 常見自訂 Hook 的命名範例:

useForm()       // 表單資料管理
useToggle()     // 開關狀態管理
useFetch()      // 發送 API 並取得資料
useScroll()     // 監聽滾動位置
useModal()      // 控制彈窗開關

這些 hook 的背後,其實就是普通的 JavaScript 函式,只不過你可以在裡面使用 useStateuseEffect 等內建 Hook,讓它能操作 React 的生命週期與狀態。

自訂 Hook vs. Function Component 有什麼差別?

雖然它們看起來都像是「函式」,但實際上用途差很多:

類型可以回傳畫面嗎?可以用其他 hook 嗎?命名規則
Function Component✅ 可以:回傳 JSX 畫面✅ 可以可以自由命名(像 App、MyCard)
Custom Hook❌ 不可以:只負責邏輯✅ 可以必須以 use 開頭(如 useXxx)

📌 換句話說:

  • Function Component 是負責「畫面顯示」的單位,你會在裡面回傳 <div>...</div>
  • Custom Hook 是負責「資料邏輯」的單位,你會在裡面寫 useState()useEffect(),然後把狀態、函式「回傳給元件使用」。

自訂 Hook 的使用情境

當你開發 React 專案一段時間後,你會漸漸遇到這兩種狀況:

多個元件都會用到的邏輯

這是最常見的使用情境之一:你寫的邏輯在很多地方都會出現,例如:

功能說明
✅ 表單處理表單欄位的輸入監聽、錯誤提示、送出處理,常常在很多頁面重複出現。
✅ API 請求頁面初始化就要發送請求、處理 loading 狀態與錯誤處理,是幾乎所有畫面的標配。
✅ 開關狀態控制是否展開(像是手風琴、Modal、下拉選單),幾乎每個 UI 元件都會用到。
✅ 滾動監聽例如 sticky 元件、滾到最底載入更多、顯示「回到頂部」按鈕等功能。

👉 這些邏輯雖然功能簡單,卻會在不同頁面或元件中反覆撰寫。

與其每次都複製貼上,不如寫成一個自訂 Hook:

const [value, toggle] = useToggle();
const { form, handleChange } = useForm({ name: '', email: '' });
const { data, loading } = useFetch('/api/user');

用法簡潔、邏輯集中、好改又好維護。

原本寫在元件裡的邏輯太雜太亂

有些功能不是因為「很多地方用」,而是「單一元件內就包含了太多事情」。

舉個例子:你有一個 ProductPage 元件,要做的事情包含:

  • 根據 ID 載入商品資料
  • 顯示 loading / error 狀態
  • 監聽視窗寬度來改變排版方式
  • 點擊「加入購物車」要觸發動畫與資料更新

這時你打開 ProductPage.jsx,很可能會看到:

useState x 5
useEffect x 4
onClick handler x 3

➡️ 這樣會讓元件膨脹成一團邏輯大雜燴,不但難維護,還會造成測試困難。

這時就可以用自訂 Hook 拆邏輯

const product = useProductData(id);
const isMobile = useBreakpoint();
const { addToCart } = useCart();

這樣 ProductPage 就能專注負責「畫面渲染」,所有複雜邏輯都交給 Hook 去處理,讓程式碼清爽又容易理解。

小結論

你可以這樣判斷是否該寫自訂 Hook:

✅ 當你發現某段邏輯會被多個元件用到
✅ 當你發現元件內的邏輯越寫越雜,開始不好維護

這時,就值得停下來思考:這段邏輯能不能抽出來變成一個 Hook?

自訂 Hook 的目標不是「讓你多寫一個函式」,而是幫你把重複與混亂變成模組化與清晰。這就是現代 React 組件開發的核心精神 💡


實作範例:三個最常見也最實用的自訂 Hook

React 中的 Hook 不只限於官方提供的 useStateuseEffect 等,我們也可以根據實際需求自行封裝「自訂 Hook」。自訂 Hook 最大的好處在於:

  • ✅ 將重複的邏輯抽離出來
  • ✅ 提高元件的可讀性與可維護性
  • ✅ 鼓勵「關注點分離」(Separation of Concerns)

以下介紹三個在實務中非常常見、也最適合初學者實作的自訂 Hook。

範例 1:useToggle — 控制布林開關的最佳利器

🎯 使用場景:

useToggle 是一個最簡單也最實用的自訂 Hook,適合用在以下狀況:

  • 控制 Modal 是否開啟
  • 展開/收合 FAQ 區塊(手風琴)
  • 切換開關、Checkbox、Switch 狀態

📦 Hook 實作:

import { useState } from 'react';

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = () => setValue((v) => !v); // 每次切換布林值
  return [value, toggle];
}

💡 使用方式:

const [isOpen, toggleOpen] = useToggle(); // 預設為 false

return (
  <button onClick={toggleOpen}>
    {isOpen ? '關閉選單' : '開啟選單'}
  </button>
);

📘 詳細說明:

你可以把 useToggle 想成是「一個負責控制開關的工具」。

平常我們在寫 React 時,如果要控制一個東西「要不要打開」,像是:

  • 彈跳視窗(Modal)開不開?
  • 功能選單展不展開?
  • Checkbox 有沒有被勾選?

你可能會這樣寫:

const [isOpen, setIsOpen] = useState(false);

每次都要寫 setIsOpen(!isOpen) 才能切換,時間久了會很重複。

但如果你用 useToggle,它幫你把「布林值切換」這件事包起來,讓你不需要重複寫邏輯:

const [isOpen, toggleOpen] = useToggle();

這樣只要一行就能做「true ➜ false ➜ true」的切換,很方便。

裡面的邏輯其實很簡單:

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = () => setValue((v) => !v); // 把 true 變 false,或把 false 變 true
  return [value, toggle];
}

你可以想像成:

  • value 是現在的開關狀態
  • toggle() 是用來切換狀態的小按鈕

這樣每次點,就會從開 ➜ 關 ➜ 開 ➜ 關,一直輪流,非常適合控制畫面上的「開關元件」。

範例 2:useForm — 多欄位表單的集中式管理

🎯 使用場景:

處理多個表單欄位時,若每個欄位都使用 useState,會讓元件變得冗長難維護。

這時 useForm 可以:

  • 集中管理整份表單資料
  • 自動更新指定欄位值
  • 避免為每個欄位各自寫一份狀態邏輯

📦 Hook 實作:

import { useState } from 'react';

function useForm(initialValues = {}) {
  const [values, setValues] = useState(initialValues);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues((prev) => ({
      ...prev,
      [name]: value,
    }));
  };

  return [values, handleChange];
}

💡 使用方式:

const [form, handleChange] = useForm({ name: '', email: '' });

return (
  <>
    <input name="name" value={form.name} onChange={handleChange} />
    <input name="email" value={form.email} onChange={handleChange} />
  </>
);

📘 詳細說明:

你可能有寫過像這樣的註冊表單:

const [name, setName] = useState('');
const [email, setEmail] = useState('');

每多一個欄位,就要再多一組 useState()。很快就會變得很亂。

這時候 useForm 就能幫你把整份表單的資料放在同一個物件裡,像這樣:

const [form, handleChange] = useForm({ name: '', email: '' });
  • form.name 裡放名字
  • form.email 裡放信箱
  • 每個輸入框只要加上 name="name"name="email",就能自己對應到對的欄位

舉例來說:

<input name="name" value={form.name} onChange={handleChange} />
<input name="email" value={form.email} onChange={handleChange} />

每次輸入時,handleChange 就會自動幫你把對應的值更新起來,不用再一個一個欄位分開管理。

它的運作方式:

function useForm(initialValues = {}) {
  const [values, setValues] = useState(initialValues);

  const handleChange = (e) => {
    const { name, value } = e.target; // 拿到欄位的 name 跟輸入的內容
    setValues((prev) => ({
      ...prev,
      [name]: value, // 根據 name 更新對應的值
    }));
  };

  return [values, handleChange];
}

這就像你有一張「表單資料表」,裡面每個欄位都有名字,使用者每打一次字,這張表就自動更新,不用你手動去設定。

範例 3:useFetch — 資料請求與 loading 狀態封裝

🎯 使用場景:

元件一進來就要從 API 抓資料,是很多應用都會遇到的情境。

這時 useFetch 可以幫你:

  • 自動發送請求
  • 處理 loading 狀態與資料
  • 封裝 fetch 流程,讓元件更專注於畫面呈現

📦 Hook 實作:

import { useEffect, useState } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true); // 載入開始
    fetch(url)
      .then((res) => res.json())
      .then((json) => setData(json)) // 資料設定
      .finally(() => setLoading(false)); // 載入結束
  }, [url]);

  return { data, loading };
}

💡 使用方式:

const { data, loading } = useFetch('/api/products');

return loading ? (
  <p>載入中...</p>
) : (
  <div>{data.name}</div>
);

📘 詳細說明:

你是不是常常寫這種情況:

「元件一打開,就要從 API 抓資料來顯示。」

比如:

  • 顯示一張產品資料卡片
  • 載入文章內容
  • 取得天氣、匯率、即時資料…

你可能會寫一堆 useEffect + fetch 還要設定 loading 狀態,有點麻煩對吧?

useFetch 就是幫你把這些事情統統包起來。

你只要這樣用:

const { data, loading } = useFetch('/api/products');

如果資料還沒回來,loadingtrue,可以顯示「載入中…」

資料回來之後,就會放在 data 裡:

return loading ? (
  <p>載入中...</p>
) : (
  <div>{data.name}</div>
);

它的內部在做什麼?

function useFetch(url) {
  const [data, setData] = useState(null);     // 放回來的資料
  const [loading, setLoading] = useState(true); // 控制「是否還在載入」

  useEffect(() => {
    setLoading(true); // 一開始進入就設為載入中

    fetch(url)
      .then((res) => res.json()) // 把回應轉成 JSON
      .then((json) => setData(json)) // 放進 data 裡
      .finally(() => setLoading(false)); // 不管成功或失敗,都把 loading 設為 false
  }, [url]); // url 變動就會重新發送

  return { data, loading };
}

你只要提供一個網址,它會自動幫你抓資料、顯示「載入中」、回傳結果,整個畫面可以寫得很乾淨。


自訂 Hook 的命名與使用規則

自訂 Hook 的設計看起來就像是普通函式,但其實它有一套明確的「命名規則」與「使用限制」,這些規範不是為了麻煩你,而是讓 React 能夠正確追蹤 Hook 的執行順序與狀態管理。

命名規則:一定要以 use 開頭

React 官方規定:「所有 Hook,不管是官方提供的還是你自己寫的,都必須以 use 開頭命名。

例如:

✅ 正確命名方式:

useToggle()
useForm()
useScroll()
useModal()

❌ 錯誤命名方式(不要這樣):

toggleHook()     // ❌ React 不會當作 Hook 看待
formManager()    // ❌ 可能導致錯誤或行為異常

🔍 為什麼這麼重要?

React 背後有一個 Hook 機制,會記錄 hook 的「呼叫順序」來維持狀態一致性。

這個機制會在執行時透過函式名稱判斷哪些是 hook。

如果你沒有用 use 開頭,React 就無法正確追蹤這個函式的狀態更新行為,可能導致錯誤或資料錯亂。

使用規則:和內建 Hook 完全一樣

自訂 Hook 本質上還是 Hook,所以也必須遵守所有官方 Hook 的使用限制:

1️⃣ 只能在函式元件或其他 Hook 裡面使用

這代表你不能在一般函式、class 元件或事件處理器中直接使用 Hook

✅ 正確:

function MyComponent() {
  const [isOpen, toggleOpen] = useToggle(); // ✅ 在函式元件中使用
}

❌ 錯誤:

function runLogic() {
  const [isOpen, toggleOpen] = useToggle(); // ❌ 普通函式中不能用 hook
}

2️⃣ 不能寫在 if、for、巢狀函式中

這是 React 最重要的 hook 使用規則之一:Hook 呼叫的順序必須一致

如果你在條件或迴圈中使用 hook,就可能導致每次 render 時呼叫順序不同,造成嚴重錯誤。

✅ 正確(Hook 寫在頂層):

function MyComponent() {
  const [isOpen, toggleOpen] = useToggle(); // ✅ 一開始就呼叫

  return isOpen ? <Modal /> : null;
}

❌ 錯誤(條件式中使用 Hook):

function MyComponent() {
  if (someCondition) {
    const [isOpen, toggleOpen] = useToggle(); // ❌ 不允許
  }
}

💥 錯誤範例可能導致的 React 錯誤訊息:

React has encountered a hook call that is not in the top level of a function component.

這表示你的 hook 呼叫方式違反規則,React 無法正確追蹤狀態。

小技巧:用 ESLint 幫你檢查 Hook 規則

React 官方提供了一個 ESLint 插件 eslint-plugin-react-hooks,可以自動幫你偵測違反 hook 規則的地方,建議在專案中啟用:

npm install eslint-plugin-react-hooks --save-dev

並在 .eslintrc 設定:

"plugins": ["react-hooks"],
"rules": {
  "react-hooks/rules-of-hooks": "error",
  "react-hooks/exhaustive-deps": "warn"
}

小總結

重點說明
自訂 Hook 是什麼?把邏輯封裝起來的函式,幫助共用、簡化元件程式碼
命名規則必須以 use 開頭
使用時機當邏輯需要重用、元件太雜、或想抽離複雜行為時
搭配方式可以結合多個內建 hook,做出完整邏輯流程

Similar Posts