3.2.5 封装你自己的积木——自定义 Hooks
一句话破题
自定义 Hook 是以 use 开头的函数,它让你将有状态逻辑提取出来,在多个组件间复用。
核心价值
当你发现多个组件有相似的状态逻辑时,不要复制粘贴——把它提取成自定义 Hook。这不仅减少重复代码,还让逻辑更易测试和维护。
自定义 Hook 基础
tsx
// hooks/useCounter.ts
import { useState, useCallback } from 'react'
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue)
const increment = useCallback(() => setCount(c => c + 1), [])
const decrement = useCallback(() => setCount(c => c - 1), [])
const reset = useCallback(() => setCount(initialValue), [initialValue])
return { count, increment, decrement, reset }
}
// 使用
function Counter() {
const { count, increment, decrement } = useCounter(10)
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}常用自定义 Hooks 模式
1. useToggle - 布尔切换
tsx
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 }
}
// 使用
const { value: isOpen, toggle } = useToggle()2. useLocalStorage - 持久化存储
tsx
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setValue = useCallback((value: T | ((val: T) => T)) => {
setStoredValue(prev => {
const valueToStore = value instanceof Function ? value(prev) : value
window.localStorage.setItem(key, JSON.stringify(valueToStore))
return valueToStore
})
}, [key])
return [storedValue, setValue] as const
}3. useFetch - 数据获取
tsx
interface UseFetchResult<T> {
data: T | null
loading: boolean
error: Error | null
refetch: () => void
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
const fetchData = useCallback(async () => {
setLoading(true)
setError(null)
try {
const res = await fetch(url)
if (!res.ok) throw new Error('Fetch failed')
const json = await res.json()
setData(json)
} catch (e) {
setError(e as Error)
} finally {
setLoading(false)
}
}, [url])
useEffect(() => {
fetchData()
}, [fetchData])
return { data, loading, error, refetch: fetchData }
}4. useDebounce - 防抖
tsx
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay)
return () => clearTimeout(timer)
}, [value, delay])
return debouncedValue
}
// 使用:搜索输入防抖
function Search() {
const [query, setQuery] = useState('')
const debouncedQuery = useDebounce(query, 300)
useEffect(() => {
if (debouncedQuery) {
// 发起搜索请求
}
}, [debouncedQuery])
}命名规范
- 必须以
use开头:这是 React 识别 Hook 的方式 - 描述功能而非实现:
useAuth而非useAuthState - 返回值要一致:对象用于多返回值,元组用于类似 useState 的场景
何时提取自定义 Hook
- 逻辑重复:两个以上组件有相同的状态逻辑
- 逻辑复杂:组件内 Hook 调用超过 5-6 个
- 关注点分离:业务逻辑和 UI 逻辑混在一起
- 便于测试:需要单独测试某段逻辑
AI 协作指南
核心意图:让 AI 帮你识别和提取可复用的逻辑。
需求定义公式:
- 功能描述:我有 [相似逻辑] 在多个组件中重复
- 交互方式:这段逻辑需要 [输入参数]
- 预期效果:Hook 返回 [哪些数据和方法]
关键术语:useCallback、useMemo、泛型、返回类型
交互策略:
- 把重复的代码展示给 AI
- 让它分析可提取的公共逻辑
- 设计 Hook 的输入输出接口
- 实现并在原组件中使用
避坑指南
- Hook 规则适用于自定义 Hook:不能条件调用、循环调用
- 不要过早抽象:等逻辑真的重复了再提取
- 保持 Hook 职责单一:一个 Hook 做一件事
- 注意闭包陷阱:回调函数记得用 useCallback
验收清单
- [ ] Hook 名称以
use开头 - [ ] 返回值类型定义清晰
- [ ] 内部回调用 useCallback 包裹
- [ ] 有明确的使用场景和文档
