自定义 React Hook 设计模式与最佳实践
自定义 Hook 是复用 React 逻辑的最强大工具。本文系统讲解设计和优化自定义 Hook 的最佳实践。
1. Hook 基础回顾
Hook 的规则
// ✅ 规则 1: 只在函数式组件顶层调用 Hook
function Component() {
const [count, setCount] = useState(0) // ✅
return <div>{count}</div>
}
// ❌ 错误: 在条件中调用
function BadComponent() {
if (true) {
const [count, setCount] = useState(0) // ❌
}
}
// ✅ 规则 2: 只在 React 函数式组件或自定义 Hook 中调用
const useCustom = () => {
const [count, setCount] = useState(0) // ✅ 在自定义 Hook 中
return count
}
// ❌ 错误: 在普通函数中调用
const normalFunction = () => {
const [count, setCount] = useState(0) // ❌
}
执行顺序很关键
// Hook 调用顺序必须一致
function Component({ showAge }: { showAge: boolean }) {
const [name, setName] = useState('') // ✅ 第 1 个
// ❌ 错误: 条件调用会破坏顺序
// if (showAge) {
// const [age, setAge] = useState(0) // 第 ? 个
// }
// ✅ 正确: 总是调用,但可能不使用
const [age, setAge] = useState(0) // ✅ 第 2 个
return <div>{name}</div>
}
2. 常用自定义 Hook 模式
模式 1: useAsync - 异步数据获取
interface AsyncState<T> {
data: T | null
loading: boolean
error: Error | null
}
function useAsync<T>(
asyncFunction: () => Promise<T>,
immediate: boolean = true
): AsyncState<T> & { execute: () => Promise<void> } {
const [state, setState] = useState<AsyncState<T>>({
data: null,
loading: immediate,
error: null
})
const execute = useCallback(async () => {
setState({ data: null, loading: true, error: null })
try {
const result = await asyncFunction()
setState({ data: result, loading: false, error: null })
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error : new Error(String(error))
})
}
}, [asyncFunction])
useEffect(() => {
if (immediate) {
execute()
}
}, [execute, immediate])
return { ...state, execute }
}
// 使用例子
function UserProfile({ userId }: { userId: number }) {
const { data: user, loading, error, execute } = useAsync(
() => fetch(`/api/users/${userId}`).then(r => r.json()),
true
)
if (loading) return <div>加载中...</div>
if (error) return <div>错误: {error.message}</div>
return (
<div>
<h1>{user?.name}</h1>
<button onClick={execute}>重新加载</button>
</div>
)
}
模式 2: usePrevious - 获取前一个值
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>()
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
// 使用例子
function Counter() {
const [count, setCount] = useState(0)
const prevCount = usePrevious(count)
return (
<div>
<p>现在: {count}</p>
<p>之前: {prevCount}</p>
<p>变化: {count - (prevCount ?? 0)}</p>
<button onClick={() => setCount(count + 1)}>增加</button>
</div>
)
}
模式 3: useDebounce - 防抖值
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(handler)
}, [value, delay])
return debouncedValue
}
// 使用例子
function SearchUsers() {
const [searchTerm, setSearchTerm] = useState('')
const debouncedSearchTerm = useDebounce(searchTerm, 500)
const { data: results } = useAsync(
() => fetch(`/api/users?q=${debouncedSearchTerm}`).then(r => r.json()),
true
)
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="搜索用户..."
/>
<ul>
{results?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}
模式 4: useThrottle - 节流值
function useThrottle<T>(value: T, delay: number): T {
const [throttledValue, setThrottledValue] = useState(value)
const lastUpdated = useRef<number | null>(null)
useEffect(() => {
const now = Date.now()
if (lastUpdated.current === null || now - lastUpdated.current >= delay) {
lastUpdated.current = now
setThrottledValue(value)
} else {
const timer = setTimeout(() => {
lastUpdated.current = Date.now()
setThrottledValue(value)
}, delay - (now - (lastUpdated.current ?? 0)))
return () => clearTimeout(timer)
}
}, [value, delay])
return throttledValue
}
// 使用例子: 监听窗口滚动
function ScrollListener() {
const [scrollPos, setScrollPos] = useState(0)
const throttledScrollPos = useThrottle(scrollPos, 200)
useEffect(() => {
const handleScroll = () => setScrollPos(window.scrollY)
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
return <div>滚动位置: {throttledScrollPos}px</div>
}
模式 5: useLocalStorage - 本地存储同步
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setValue = useCallback((value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value
setStoredValue(valueToStore)
window.localStorage.setItem(key, JSON.stringify(valueToStore))
} catch (error) {
console.error(error)
}
}, [key, storedValue])
return [storedValue, setValue]
}
// 使用例子
function UserPreferences() {
const [theme, setTheme] = useLocalStorage('theme', 'light')
const [fontSize, setFontSize] = useLocalStorage('fontSize', 16)
return (
<div style={{ background: theme === 'light' ? '#fff' : '#333' }}>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option>light</option>
<option>dark</option>
</select>
<input
type="number"
value={fontSize}
onChange={(e) => setFontSize(Number(e.target.value))}
/>
</div>
)
}
3. 性能优化 Hook
useCallback - 缓存回调函数
// ❌ 问题: 每次渲染都创建新的回调函数
function Parent() {
const [data, setData] = useState([])
const handleAdd = () => {
setData([...data, Math.random()])
}
// Child 会因为 handleAdd 改变而重新渲染
return <Child onAdd={handleAdd} />
}
// ✅ 解决: 用 useCallback 缓存
function Parent2() {
const [data, setData] = useState([])
const handleAdd = useCallback(() => {
setData(prev => [...prev, Math.random()])
}, []) // 依赖项为空,函数不会改变
return <Child onAdd={handleAdd} />
}
// 正确的依赖项
function Parent3() {
const [multiplier, setMultiplier] = useState(1)
const handleAdd = useCallback((value: number) => {
// 使用 multiplier,必须加到依赖项
return value * multiplier
}, [multiplier]) // 当 multiplier 变化时,函数会重新创建
}
useMemo - 缓存计算结果
// ❌ 问题: 复杂计算每次都执行
function ExpensiveComponent({ items }: { items: Item[] }) {
const sorted = items
.filter(item => item.active)
.sort((a, b) => b.priority - a.priority)
.map(item => ({
...item,
score: calculateComplexScore(item)
}))
return <List data={sorted} />
}
// ✅ 解决: 用 useMemo 缓存结果
function ExpensiveComponent2({ items }: { items: Item[] }) {
const sorted = useMemo(() => {
return items
.filter(item => item.active)
.sort((a, b) => b.priority - a.priority)
.map(item => ({
...item,
score: calculateComplexScore(item)
}))
}, [items]) // items 变化时才重新计算
return <List data={sorted} />
}
// 合并 useCallback 和 useMemo
function SearchResults({ query }: { query: string }) {
const results = useMemo(() => {
return performExpensiveSearch(query)
}, [query])
const handleSelect = useCallback((item: Item) => {
// 处理选择
}, [])
return <ResultsList items={results} onSelect={handleSelect} />
}
React.memo + useCallback 的组合
interface ChildProps {
items: Item[]
onSelect: (item: Item) => void
}
// 使用 React.memo 防止不必要重渲染
const MemoChild = React.memo(function Child({ items, onSelect }: ChildProps) {
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onSelect(item)}>
{item.name}
</li>
))}
</ul>
)
})
function Parent() {
const [selectedId, setSelectedId] = useState<number | null>(null)
const [items, setItems] = useState<Item[]>([])
// useCallback 很重要: 如果不用,每次 Parent 渲染时创建新的 handleSelect
// 导致 MemoChild 无法避免重渲染
const handleSelect = useCallback((item: Item) => {
setSelectedId(item.id)
}, [])
return <MemoChild items={items} onSelect={handleSelect} />
}
4. 高级模式
模式 1: useReducer - 复杂状态管理
interface State {
count: number
error: string | null
history: number[]
}
type Action =
| { type: 'INCREMENT'; payload: number }
| { type: 'DECREMENT'; payload: number }
| { type: 'RESET' }
| { type: 'ERROR'; payload: string }
function countReducer(state: State, action: Action): State {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + action.payload,
history: [...state.history, state.count]
}
case 'DECREMENT':
return {
...state,
count: state.count - action.payload,
history: [...state.history, state.count]
}
case 'RESET':
return { count: 0, error: null, history: [] }
case 'ERROR':
return { ...state, error: action.payload }
default:
return state
}
}
function Counter() {
const [state, dispatch] = useReducer(countReducer, {
count: 0,
error: null,
history: []
})
return (
<div>
<p>计数: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT', payload: 1 })}>
+1
</button>
<button onClick={() => dispatch({ type: 'DECREMENT', payload: 1 })}>
-1
</button>
<button onClick={() => dispatch({ type: 'RESET' })}>
重置
</button>
{state.error && <p style={{ color: 'red' }}>{state.error}</p>}
<p>历史: {state.history.join(', ')}</p>
</div>
)
}
模式 2: useContext + useReducer - 全局状态
interface AppState {
user: User | null
loading: boolean
}
type AppAction =
| { type: 'SET_USER'; payload: User }
| { type: 'LOGOUT' }
const AppContext = createContext<AppState | null>(null)
const AppDispatchContext = createContext<React.Dispatch<AppAction> | null>(null)
function AppProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(appReducer, {
user: null,
loading: false
})
return (
<AppContext.Provider value={state}>
<AppDispatchContext.Provider value={dispatch}>
{children}
</AppDispatchContext.Provider>
</AppContext.Provider>
)
}
// 自定义 Hook: 使用全局状态
function useAppState() {
const state = useContext(AppContext)
if (!state) throw new Error('useAppState 必须在 AppProvider 内使用')
return state
}
function useAppDispatch() {
const dispatch = useContext(AppDispatchContext)
if (!dispatch) throw new Error('useAppDispatch 必须在 AppProvider 内使用')
return dispatch
}
// 使用
function Header() {
const { user } = useAppState()
const dispatch = useAppDispatch()
return (
<div>
{user ? (
<>
<p>欢迎, {user.name}</p>
<button onClick={() => dispatch({ type: 'LOGOUT' })}>
退出
</button>
</>
) : (
<p>未登录</p>
)}
</div>
)
}
5. Hook 最佳实践检查清单
// ✅ Hook 设计检查清单
// 1. 命名规范: use 开头
function useCustomHook() { // ✅
// ...
}
// 2. 单一职责: 一个 Hook 做一件事
function useUserData() { // ✅ 只负责获取用户数据
// ...
}
// 3. 返回类型明确
function useCustom(): { value: string; setValue: (v: string) => void } { // ✅
// ...
}
// 4. 依赖项完整: 所有使用的值都在依赖项中
function useHook() {
const [count, setCount] = useState(0)
const external = useContext(SomeContext)
useEffect(() => {
// 使用了 count 和 external
console.log(count, external)
}, [count, external]) // ✅ 两者都包含
}
// 5. 清理副作用
function useHook() {
useEffect(() => {
const listener = () => {}
window.addEventListener('resize', listener)
return () => { // ✅ 清理监听器
window.removeEventListener('resize', listener)
}
}, [])
}
// 6. 避免在 Hook 中创建新对象作为依赖
// ❌ 错误
function useHook(config: { value: number }) {
useEffect(() => {
// ...
}, [config]) // config 每次都是新对象,导致无限循环
}
// ✅ 正确
function useHook(configValue: number) {
useEffect(() => {
// ...
}, [configValue]) // 使用原始值作为依赖
}
总结
自定义 Hook 的核心优势:
| 优势 | 说明 |
|---|---|
| 逻辑复用 | 跨组件复用有状态逻辑 |
| 代码组织 | 按功能而非生命周期组织 |
| 易于测试 | Hook 是普通函数,易于单元测试 |
| 可组合 | Hook 可以相互调用,构建复杂功能 |
| 渐进式 | 可以逐步迁移现有代码到 Hook |


