自訂 Hook 是什麼?如何封裝自己的邏輯
更新日期: 2025 年 4 月 25 日
學會基本的 React Hook(如 useState
、useEffect
)後,你可能會發現:
- 有些邏輯會在好幾個元件中重複出現
- 元件變得又長又難讀,因為邏輯與畫面混在一起
- 每次寫類似的邏輯都要重新複製貼上
這時,你就需要 自訂 Hook(Custom Hook) 來幫你「抽出邏輯」、「共用邏輯」,讓程式碼乾淨又容易維護。
什麼是自訂 Hook?
簡單定義
在 React 中,Hook 是一種可以在函式元件中使用的特殊函式,像是 useState
、useEffect
。
而自訂 Hook(Custom Hook),則是指:
🔁 「把重複使用的邏輯用函式包裝起來,讓不同元件可以共用。」
說白一點,它就像是一段你自己寫的工具程式,只不過它裡面可以使用其他 React Hook,而且必須以 use
開頭命名。
這樣命名是有目的的:React 會透過函式名稱來判斷這是不是一個 Hook,進而正確追蹤狀態更新與副作用的順序。
如果你忘了加 use
,React 可能會跳錯或無法正確執行。
👀 常見自訂 Hook 的命名範例:
useForm() // 表單資料管理
useToggle() // 開關狀態管理
useFetch() // 發送 API 並取得資料
useScroll() // 監聽滾動位置
useModal() // 控制彈窗開關
這些 hook 的背後,其實就是普通的 JavaScript 函式,只不過你可以在裡面使用 useState
、useEffect
等內建 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 不只限於官方提供的 useState
、useEffect
等,我們也可以根據實際需求自行封裝「自訂 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');
如果資料還沒回來,loading
是 true
,可以顯示「載入中…」
資料回來之後,就會放在 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,做出完整邏輯流程 |