为什么要封装自定义 Hook
React 内置的 useState、useEffect 等 Hook 解决的是”状态”和”副作用”问题,但业务里大量重复逻辑——发请求、防抖、读写本地存储——如果每个组件都写一遍,代码会迅速失控。自定义 Hook 就是把可复用的逻辑抽出来,本质是一个以 use 开头的普通函数,内部可以调用其他 Hook。
自定义 Hook 的基本规则
- 必须以
use 开头(React 靠命名识别 Hook,配合 ESLint 插件检查);
- 内部可以调用其他 Hook,也可以组合多个自定义 Hook;
- 遵循 Hook 规则:只能在组件顶层调用 Hook,不能在条件、循环里调用;
- 返回什么由自己决定,通常返回状态和操作方法。
案例一:useFetch 请求封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import { useState, useEffect, useCallback } from 'react'
export function useFetch(url) { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null)
useEffect(() => { let ignore = false
async function load() { setLoading(true) setError(null) try { const res = await fetch(url) if (!res.ok) throw new Error(`HTTP ${res.status}`) const json = await res.json() if (!ignore) setData(json) } catch (e) { if (!ignore) setError(e) } finally { if (!ignore) setLoading(false) } }
load() return () => { ignore = true } }, [url])
const refresh = useCallback(() => { setVersion((v) => v + 1) }, [])
return { data, loading, error, refresh } }
|
组件里一行代码就拿到请求状态:
1 2 3 4 5 6 7 8 9
| import { useFetch } from './useFetch'
function UserList() { const { data, loading, error } = useFetch('/api/users')
if (loading) return <div>加载中...</div> if (error) return <div>出错了:{error.message}</div> return <ul>{data?.map((u) => <li key={u.id}>{u.name}</li>)}</ul> }
|
关于竞态:请求是异步的,如果 url 很快变化或组件卸载,旧请求的结果可能覆盖新状态。上面的 ignore 标记是 React 官方推荐的防竞态写法。
案例二:useDebounce 防抖
搜索输入场景,防抖是最高频的需求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import { useState, useEffect } from 'react'
export function useDebounce(value, delay = 300) { const [debounced, setDebounced] = useState(value)
useEffect(() => { const timer = setTimeout(() => { setDebounced(value) }, delay)
return () => clearTimeout(timer) }, [value, delay])
return debounced }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { useState, useEffect } from 'react' import { useDebounce } from './useDebounce'
function SearchBox() { const [keyword, setKeyword] = useState('') const debouncedKeyword = useDebounce(keyword, 500)
useEffect(() => { if (!debouncedKeyword) return fetch(`/api/search?q=${debouncedKeyword}`) }, [debouncedKeyword])
return <input value={keyword} onChange={(e) => setKeyword(e.target.value)} /> }
|
案例三:useLocalStorage 同步本地存储
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import { useState, useEffect } from 'react'
export function useLocalStorage(key, initialValue) { const [value, setValue] = useState(() => { try { const stored = window.localStorage.getItem(key) return stored !== null ? JSON.parse(stored) : initialValue } catch { return initialValue } })
useEffect(() => { try { window.localStorage.setItem(key, JSON.stringify(value)) } catch { } }, [key, value])
return [value, setValue] }
|
1 2 3 4 5 6 7 8
| function ThemeSwitch() { const [theme, setTheme] = useLocalStorage('theme', 'light') return ( <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> 当前主题:{theme} </button> ) }
|
组合多个 Hook
自定义 Hook 可以互相组合,形成更上层的抽象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import { useState, useEffect } from 'react' import { useDebounce } from './useDebounce'
export function useSearchUsers() { const [keyword, setKeyword] = useState('') const debouncedKeyword = useDebounce(keyword, 400) const [users, setUsers] = useState([]) const [loading, setLoading] = useState(false)
useEffect(() => { if (!debouncedKeyword) { setUsers([]) return } let ignore = false setLoading(true) fetch(`/api/users?q=${debouncedKeyword}`) .then((r) => r.json()) .then((data) => { if (!ignore) setUsers(data) }) .finally(() => { if (!ignore) setLoading(false) }) return () => { ignore = true } }, [debouncedKeyword])
return { keyword, setKeyword, users, loading } }
|
常见误区
- 在自定义 Hook 里返回
useCallback/useMemo 的依赖漏写:依赖数组不全会导致闭包捕获旧值,用 eslint-plugin-react-hooks 的 exhaustive-deps 规则可以兜底;
- 把所有逻辑都塞进一个 Hook:Hook 也讲究单一职责,一个 Hook 做一件事;
- 忘记清理副作用:定时器、订阅、事件监听都要在清理函数中解除,否则组件卸载后仍会执行。
总结
自定义 Hook 让 React 组件”瘦身”、逻辑”复用”。建议按这个顺序上手:先封装 useFetch 类的基础逻辑,再封装 useDebounce 这类通用工具,最后根据业务组合出领域 Hook。命名统一 useXxx,放在 src/hooks/ 目录下管理。