React 自定義 Hook (Custom Hooks)
自定義 Hook 是一種重用狀態邏輯的方式。當你發現多個元件有相似的邏輯時,可以把這些邏輯抽取成自定義 Hook。
什麼是自定義 Hook?
自定義 Hook 就是一個以 use 開頭的函式,內部可以使用其他 Hook:
function useMyHook() {
const [state, setState] = useState(initialValue)
// 可以使用任何 Hook
useEffect(() => {
// ...
}, [])
return state
}
命名規則:
- 必須以
use開頭(例如useCounter、useFetch) - 這讓 React 知道這是一個 Hook,需要遵守 Hook 的規則
基本範例:useCounter
import { useState } from 'react'
// 自定義 Hook
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue)
const increment = () => setCount((c) => c + 1)
const decrement = () => setCount((c) => c - 1)
const reset = () => setCount(initialValue)
return { count, increment, decrement, reset }
}
// 使用自定義 Hook
function Counter() {
const { count, increment, decrement, reset } = useCounter(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Reset</button>
</div>
)
}
// 另一個元件也可以使用同一個 Hook
function AnotherCounter() {
const counter1 = useCounter(10)
const counter2 = useCounter(100)
return (
<div>
<p>Counter 1: {counter1.count}</p>
<p>Counter 2: {counter2.count}</p>
</div>
)
}
每次使用自定義 Hook,都會得到獨立的狀態。多個元件使用同一個 Hook,它們的狀態不會互相影響。
實用範例
useToggle - 開關狀態
import { useState, useCallback } from 'react'
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue)
const toggle = useCallback(() => setValue((v) => !v), [])
const setTrue = useCallback(() => setValue(true), [])
const setFalse = useCallback(() => setValue(false), [])
return { value, toggle, setTrue, setFalse }
}
// 使用
function Modal() {
const { value: isOpen, toggle, setFalse: close } = useToggle()
return (
<div>
<button onClick={toggle}>開啟 Modal</button>
{isOpen && (
<div className="modal">
<p>Modal 內容</p>
<button onClick={close}>關閉</button>
</div>
)}
</div>
)
}
useLocalStorage - 同步 localStorage
import { useState, useEffect } from 'react'
function useLocalStorage(key, initialValue) {
// 初始化時從 localStorage 讀取
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
// 當值改變時同步到 localStorage
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value))
} catch {
console.error('無法儲存到 localStorage')
}
}, [key, value])
return [value, setValue]
}
// 使用
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
const [fontSize, setFontSize] = useLocalStorage('fontSize', 16)
return (
<div>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">淺色</option>
<option value="dark">深色</option>
</select>
<input type="number" value={fontSize} onChange={(e) => setFontSize(Number(e.target.value))} />
</div>
)
}
useFetch - 資料獲取
import { useState, useEffect } from 'react'
function useFetch(url) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
async function fetchData() {
setLoading(true)
setError(null)
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const json = await response.json()
if (!cancelled) {
setData(json)
}
} catch (e) {
if (!cancelled) {
setError(e.message)
}
} finally {
if (!cancelled) {
setLoading(false)
}
}
}
fetchData()
return () => {
cancelled = true
}
}, [url])
return { data, loading, error }
}
// 使用
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`https://api.example.com/users/${userId}`)
if (loading) return <p>載入中...</p>
if (error) return <p>錯誤: {error}</p>
if (!user) return null
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
useDebounce - 防抖
import { useState, useEffect } from 'react'
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
// 使用:搜尋時減少 API 呼叫
function SearchInput() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
// 只有當 debouncedQuery 改變時才會觸發搜尋
useEffect(() => {
if (debouncedQuery) {
console.log('搜尋:', debouncedQuery)
// 執行搜尋 API 呼叫
}
}, [debouncedQuery])
return <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="搜尋..." />
}
useWindowSize - 監聽視窗大小
import { useState, useEffect } from 'react'
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
})
useEffect(() => {
function handleResize() {
setSize({
width: window.innerWidth,
height: window.innerHeight,
})
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return size
}
// 使用
function ResponsiveComponent() {
const { width, height } = useWindowSize()
return (
<div>
<p>
視窗大小: {width} x {height}
</p>
{width < 768 ? <MobileLayout /> : <DesktopLayout />}
</div>
)
}
useOnClickOutside - 點擊外部
import { useEffect, useRef } from 'react'
function useOnClickOutside(handler) {
const ref = useRef(null)
useEffect(() => {
function handleClick(event) {
if (ref.current && !ref.current.contains(event.target)) {
handler()
}
}
document.addEventListener('mousedown', handleClick)
return () => document.removeEventListener('mousedown', handleClick)
}, [handler])
return ref
}
// 使用:點擊外部關閉下拉選單
function Dropdown() {
const [isOpen, setIsOpen] = useState(false)
const ref = useOnClickOutside(() => setIsOpen(false))
return (
<div ref={ref}>
<button onClick={() => setIsOpen(!isOpen)}>選單</button>
{isOpen && (
<ul className="dropdown-menu">
<li>選項 1</li>
<li>選項 2</li>
<li>選項 3</li>
</ul>
)}
</div>
)
}
自定義 Hook 的最佳實踐
1. 單一職責
每個 Hook 只做一件事:
// ✅ 好:專注於一件事
function useDocumentTitle(title) {
useEffect(() => {
document.title = title
}, [title])
}
// ❌ 不好:做太多事
function useEverything(title, theme, user) {
// 設定標題
// 設定主題
// 獲取使用者資料
// ...
}
2. 返回有意義的值
// ✅ 好:返回有結構的資料
function useFetch(url) {
return { data, loading, error, refetch }
}
// ✅ 也可以返回陣列(像 useState)
function useToggle(initial) {
return [value, toggle]
}
3. 處理清理邏輯
function useInterval(callback, delay) {
useEffect(() => {
const id = setInterval(callback, delay)
// 記得清理
return () => clearInterval(id)
}, [callback, delay])
}
組合多個 Hook
自定義 Hook 可以使用其他自定義 Hook:
function useUser(userId) {
const { data: user, loading, error } = useFetch(`https://api.example.com/users/${userId}`)
const [isFollowing, setIsFollowing] = useLocalStorage(`following-${userId}`, false)
return {
user,
loading,
error,
isFollowing,
toggleFollow: () => setIsFollowing((f) => !f),
}
}