自定义 Hook 的设计原则是什么?什么逻辑适合抽成 Hook?从工程实践角度深入探讨 Hook 设计背后的思考模型。
1. 为什么我们需要”设计良好”的自定义 Hook
在大型 React 项目中,糟糕的 Hook 设计会带来严重后果:
- 维护成本激增:逻辑散落各处,修改一处影响多处
- 性能问题难排查:过度渲染、内存泄漏隐藏在封装之下
- 团队协作困难:Hook 职责不清,参数返回值不直观
- 测试复杂度倍增:副作用耦合,难以隔离测试
设计良好的 Hook 应该像一个精心设计的 API:
1 2 3 4 5
| const { data, loading, error, refetch } = useFetch('/api/users');
const [data, setData, loading, error, fetchAgain, cancel, retry] = useData('/api/users');
|
核心观点:Hook 不是为了”少写几行代码”,而是为了隔离关注点、提升可维护性。
2. 自定义 Hook 的本质:逻辑抽象,而不是代码复用
2.1 常见误区
很多开发者将 Hook 视为”代码复用工具”,这是一个本末倒置的认知:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function useButtonHandler() { const handleClick = () => console.log('clicked'); return handleClick; }
function useAsyncOperation<T>(asyncFn: () => Promise<T>) { const [state, setState] = useState({ loading: false, data: null, error: null }); const execute = useCallback(async () => { setState({ loading: true, data: null, error: null }); try { const data = await asyncFn(); setState({ loading: false, data, error: null }); } catch (error) { setState({ loading: false, data: null, error }); } }, [asyncFn]); return { ...state, execute }; }
|
2.2 逻辑抽象的三个层次
- 状态封装:将状态及其变更逻辑封装在一起
- 副作用隔离:将副作用与组件逻辑解耦
- 业务模型抽象:将业务逻辑抽象为可复用的问题模型
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 41 42
| function useToggle(initialValue = false) { const [value, setValue] = useState(initialValue); const toggle = useCallback(() => setValue(v => !v), []); return [value, toggle] as const; }
function useInterval(callback: () => void, delay: number | null) { const savedCallback = useRef(callback); useEffect(() => { savedCallback.current = callback; }, [callback]); useEffect(() => { if (delay === null) return; const id = setInterval(() => savedCallback.current(), delay); return () => clearInterval(id); }, [delay]); }
function usePagination<T>(fetchFn: (page: number) => Promise<T[]>) { const [page, setPage] = useState(1); const { data, loading, error, execute } = useAsyncOperation( () => fetchFn(page) ); useEffect(() => { execute(); }, [page, execute]); return { data, loading, error, page, nextPage: () => setPage(p => p + 1), prevPage: () => setPage(p => Math.max(1, p - 1)), }; }
|
3. Hook 的设计原则(工程级)
3.1 单一职责原则
每个 Hook 应该只做一件事,且做好这一件事。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function useUserProfile() { const [user, setUser] = useState(null); const [posts, setPosts] = useState([]); const [theme, setTheme] = useState('light'); const [notifications, setNotifications] = useState([]); }
function useUser() { const [user, setUser] = useState(null); }
function useUserPosts(userId: string) { }
function useTheme() { }
|
判断标准:Hook 的名字是否能用一个动词或名词准确描述?如果需要用”和”连接多个动词,说明职责不单一。
3.2 可组合性
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 33 34 35 36 37 38 39 40 41 42
| function useLocalStorage<T>(key: string, initialValue: T) { const [value, setValue] = useState<T>(() => { const stored = localStorage.getItem(key); return stored ? JSON.parse(stored) : initialValue; }); useEffect(() => { localStorage.setItem(key, JSON.stringify(value)); }, [key, value]); return [value, setValue] as const; }
function useAuth() { const [token, setToken] = useLocalStorage('auth_token', null); const [user, setUser] = useLocalStorage('user', null); const login = useCallback(async (credentials) => { const { token, user } = await api.login(credentials); setToken(token); setUser(user); }, [setToken, setUser]); const logout = useCallback(() => { setToken(null); setUser(null); }, [setToken, setUser]); return { token, user, login, logout }; }
function useProtectedRoute() { const { token } = useAuth(); const navigate = useNavigate(); useEffect(() => { if (!token) navigate('/login'); }, [token, navigate]); }
|
设计建议:先设计小而专注的 Hook,再通过组合构建复杂功能。
3.3 稳定性(引用、返回值)
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
| function useBadCounter() { const [count, setCount] = useState(0); return { count, increment: () => setCount(c => c + 1), decrement: () => setCount(c => c - 1), }; }
function useGoodCounter() { const [count, setCount] = useState(0); const increment = useCallback(() => setCount(c => c + 1), []); const decrement = useCallback(() => setCount(c => c - 1), []); return { count, increment, decrement }; }
function useBetterCounter() { const [count, setCount] = useState(0); const increment = useCallback(() => setCount(c => c + 1), []); const decrement = useCallback(() => setCount(c => c - 1), []); return useMemo( () => ({ count, increment, decrement }), [count, increment, decrement] ); }
|
核心原则:如果 Hook 返回的值会被用作其他 Hook 的依赖,必须保证引用稳定。
3.4 边界清晰
Hook 的职责边界和副作用范围应该清晰明确。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function useBadForm(externalState: any) { const [value, setValue] = useState(''); useEffect(() => { externalState.formData = value; }, [value]); return [value, setValue]; }
function useGoodForm(onValueChange?: (value: string) => void) { const [value, setValue] = useState(''); const handleChange = useCallback((newValue: string) => { setValue(newValue); onValueChange?.(newValue); }, [onValueChange]); return [value, handleChange] as const; }
|
设计规范:
- Hook 内部状态自己管理
- 外部状态通过参数传入、通过回调传出
- 不要让 Hook “伸手”去改外部状态
3.5 参数与返回值设计哲学
参数设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function useFetch( url: string, method: string, headers: object, body: any, retry: number, timeout: number, onSuccess: Function, onError: Function ) { }
function useFetch<T>(url: string, options?: { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; headers?: Record<string, string>; body?: any; retry?: number; timeout?: number; onSuccess?: (data: T) => void; onError?: (error: Error) => void; }) { }
|
参数设计原则:
- 必需参数放前面,可选参数放后面
- 超过 3 个参数考虑使用配置对象
- 提供合理的默认值
- 使用 TypeScript 类型约束
返回值设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function useBadFetch(url: string) { return [data, loading, error, refetch, cancel]; }
function useGoodFetch<T>(url: string) { return { data: T | null, loading: boolean, error: Error | null, refetch: () => void, cancel: () => void, }; }
function useToggle(initial = false) { return [value, toggle] as const; }
|
返回值选择规则:
- 2 个值以内:数组(模仿 useState 风格)
- 3 个值以上:对象(语义更清晰)
- 同类数据:对象包裹(如
{ data, loading, error })
3.6 依赖管理
依赖数组是 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 33 34 35 36
| function useBadInterval(callback: () => void, delay: number) { useEffect(() => { const id = setInterval(callback, delay); return () => clearInterval(id); }, [delay]); }
function useGoodInterval(callback: () => void, delay: number) { const savedCallback = useRef(callback); useEffect(() => { savedCallback.current = callback; }, [callback]); useEffect(() => { const id = setInterval(() => savedCallback.current(), delay); return () => clearInterval(id); }, [delay]); }
function useDebounce<T>(value: T, delay: number) { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => clearTimeout(handler); }, [value, delay]); return debouncedValue; }
|
依赖管理原则:
- 不要忽略 ESLint 的 exhaustive-deps 警告
- 对于回调函数,考虑使用 ref 存储最新值
- 对于复杂对象,考虑使用 useMemo 稳定引用
- 明确每个依赖的作用和变化时机
4. 抽象边界:什么该进 Hook,什么不该
4.1 适合抽成 Hook 的场景
✅ 场景 1:有状态的业务逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function useFormValidation<T>(initialValues: T, rules: ValidationRules<T>) { const [values, setValues] = useState(initialValues); const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({}); const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({}); const validate = useCallback((fieldName: keyof T) => { const rule = rules[fieldName]; const error = rule ? rule(values[fieldName]) : null; setErrors(prev => ({ ...prev, [fieldName]: error })); return !error; }, [values, rules]); return { values, errors, touched, setValues, validate }; }
|
✅ 场景 2:副作用封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function useEventListener<K extends keyof WindowEventMap>( eventName: K, handler: (event: WindowEventMap[K]) => void, element: HTMLElement | Window = window ) { const savedHandler = useRef(handler); useEffect(() => { savedHandler.current = handler; }, [handler]); useEffect(() => { const eventListener = (event: Event) => savedHandler.current(event as WindowEventMap[K]); element.addEventListener(eventName, eventListener); return () => element.removeEventListener(eventName, eventListener); }, [eventName, element]); }
|
✅ 场景 3:复杂的派生状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function useCart() { const [items, setItems] = useState<CartItem[]>([]); const summary = useMemo(() => ({ total: items.reduce((sum, item) => sum + item.price * item.quantity, 0), count: items.reduce((sum, item) => sum + item.quantity, 0), tax: items.reduce((sum, item) => sum + item.price * item.quantity * 0.1, 0), }), [items]); const addItem = useCallback((item: CartItem) => { setItems(prev => [...prev, item]); }, []); return { items, summary, addItem }; }
|
4.2 不适合抽成 Hook 的场景
❌ 场景 1:纯函数计算
1 2 3 4 5 6 7 8 9
| function useFormatPrice(price: number) { return `$${price.toFixed(2)}`; }
function formatPrice(price: number) { return `$${price.toFixed(2)}`; }
|
❌ 场景 2:一次性的简单操作
1 2 3 4 5 6 7
| function useButtonClick(callback: () => void) { return () => callback(); }
<button onClick={handleClick}>Click</button>
|
❌ 场景 3:强耦合的 UI 逻辑
1 2 3 4 5 6 7 8 9
| function useModalLayout() { return { modalStyle: { position: 'fixed', top: '50%' }, overlayStyle: { background: 'rgba(0,0,0,0.5)' }, }; }
|
4.3 判断标准
使用”四问法”判断是否应该抽 Hook:
- 是否有状态? 如果只是纯函数计算,不需要 Hook
- 是否有副作用? 如果没有订阅/清理/DOM 操作,可能不需要 Hook
- 是否需要复用? 如果只用一次,先在组件内实现
- 是否逻辑独立? 如果与具体 UI 强耦合,不适合抽 Hook
5. 常见 Hook 设计模式及其背后的问题模型
5.1 异步状态管理模式
问题模型:异步操作有三种状态(loading/success/error),需要统一处理。
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
| function useAsync<T>(asyncFunction: () => Promise<T>, immediate = true) { const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); const [data, setData] = useState<T | null>(null); const [error, setError] = useState<Error | null>(null); const execute = useCallback(async () => { setStatus('loading'); setData(null); setError(null); try { const response = await asyncFunction(); setData(response); setStatus('success'); return response; } catch (error) { setError(error as Error); setStatus('error'); throw error; } }, [asyncFunction]); useEffect(() => { if (immediate) { execute(); } }, [execute, immediate]); return { execute, status, data, error }; }
|
适用场景:API 调用、数据加载、表单提交
5.2 订阅-清理模式
问题模型:需要订阅外部数据源,并在组件卸载时清理。
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
| function useWebSocket(url: string) { const [data, setData] = useState(null); const [readyState, setReadyState] = useState<number>(WebSocket.CONNECTING); const ws = useRef<WebSocket | null>(null); useEffect(() => { ws.current = new WebSocket(url); ws.current.onopen = () => setReadyState(WebSocket.OPEN); ws.current.onclose = () => setReadyState(WebSocket.CLOSED); ws.current.onmessage = (event) => setData(JSON.parse(event.data)); return () => { ws.current?.close(); }; }, [url]); const send = useCallback((message: any) => { if (ws.current?.readyState === WebSocket.OPEN) { ws.current.send(JSON.stringify(message)); } }, []); return { data, readyState, send }; }
|
适用场景:WebSocket、EventSource、定时器、事件监听
5.3 防抖/节流模式
问题模型:限制函数调用频率,避免性能问题。
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
| function useDebounce<T>(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState<T>(value); useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(handler); }; }, [value, delay]); return debouncedValue; }
function useDebouncedCallback<T extends (...args: any[]) => any>( callback: T, delay: number ): T { const timeoutRef = useRef<NodeJS.Timeout>(); const callbackRef = useRef(callback); useEffect(() => { callbackRef.current = callback; }, [callback]); return useCallback( ((...args) => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } timeoutRef.current = setTimeout(() => { callbackRef.current(...args); }, delay); }) as T, [delay] ); }
|
适用场景:搜索输入、滚动事件、窗口 resize
5.4 本地存储同步模式
问题模型:将状态持久化到 localStorage,并在多标签页间同步。
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
| function useLocalStorage<T>(key: string, initialValue: T) { const [storedValue, setStoredValue] = useState<T>(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error(error); 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]); useEffect(() => { const handleStorageChange = (e: StorageEvent) => { if (e.key === key && e.newValue) { setStoredValue(JSON.parse(e.newValue)); } }; window.addEventListener('storage', handleStorageChange); return () => window.removeEventListener('storage', handleStorageChange); }, [key]); return [storedValue, setValue] as const; }
|
适用场景:用户偏好、主题设置、表单草稿
5.5 生命周期模式
问题模型:在特定生命周期执行操作。
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
| function useMount(callback: () => void) { useEffect(() => { callback(); }, []); }
function useUnmount(callback: () => void) { const callbackRef = useRef(callback); callbackRef.current = callback; useEffect(() => { return () => callbackRef.current(); }, []); }
function usePrevious<T>(value: T): T | undefined { const ref = useRef<T>(); useEffect(() => { ref.current = value; }, [value]); return ref.current; }
|
适用场景:数据预加载、分析上报、资源清理
6. 工程实践中的坑与代价
6.1 闭包陷阱
问题描述:useEffect/useCallback 中访问到的是旧的闭包变量。
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 41 42 43 44 45 46 47 48 49 50 51
| function Counter() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { console.log(count); setCount(count + 1); }, 1000); return () => clearInterval(timer); }, []); return <div>{count}</div>; }
function Counter() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { setCount(c => c + 1); }, 1000); return () => clearInterval(timer); }, []); return <div>{count}</div>; }
function Counter() { const [count, setCount] = useState(0); const countRef = useRef(count); useEffect(() => { countRef.current = count; }, [count]); useEffect(() => { const timer = setInterval(() => { console.log(countRef.current); setCount(countRef.current + 1); }, 1000); return () => clearInterval(timer); }, []); return <div>{count}</div>; }
|
最佳实践:
- 优先使用函数式更新
setState(prev => ...)
- 需要访问多个状态时使用
useReducer
- 对于回调函数使用 ref 存储最新值
6.2 依赖数组陷阱
问题描述:依赖数组管理不当导致无限循环或状态不同步。
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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| function useBadEffect() { const [count, setCount] = useState(0); const config = { url: '/api/data' }; useEffect(() => { fetch(config.url).then(); }, [config]); }
function useGoodEffect() { const [count, setCount] = useState(0); const config = useMemo(() => ({ url: '/api/data' }), []); useEffect(() => { fetch(config.url).then(); }, [config]); }
function useBetterEffect() { const [count, setCount] = useState(0); const url = '/api/data'; useEffect(() => { fetch(url).then(); }, [url]); }
function SearchBox({ onSearch }: { onSearch: (keyword: string) => void }) { const [keyword, setKeyword] = useState(''); useEffect(() => { const timer = setTimeout(() => { onSearch(keyword); }, 500); return () => clearTimeout(timer); }, [keyword]); }
function SearchBox({ onSearch }: { onSearch: (keyword: string) => void }) { const [keyword, setKeyword] = useState(''); useEffect(() => { const timer = setTimeout(() => { onSearch(keyword); }, 500); return () => clearTimeout(timer); }, [keyword, onSearch]); }
function Parent() { const handleSearch = useCallback((keyword: string) => { console.log('Search:', keyword); }, []); return <SearchBox onSearch={handleSearch} />; }
|
最佳实践:
- 开启 ESLint
react-hooks/exhaustive-deps 规则
- 对象/数组依赖用
useMemo 稳定
- 函数依赖用
useCallback 稳定
- 考虑使用
useReducer 替代多个 useState
6.3 过度封装陷阱
问题描述:为了”代码复用”而过度抽象,导致难以理解和维护。
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 41 42 43 44
| function useDataWithCacheAndRetryAndPagination<T>( url: string, options: ComplexOptions ) { const cache = useCache(); const retry = useRetry(options.retryConfig); const pagination = usePagination(options.page); const fetcher = useFetcher(url, { cache, retry, pagination }); const transformer = useDataTransformer(options.transform); const validator = useDataValidator(options.validate); return { data: transformer(validator(fetcher.data)), }; }
function useFetch<T>(url: string) { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(false); const [error, setError] = useState<Error | null>(null); }
function useFetchWithCache<T>(url: string) { const { data, loading, error } = useFetch<T>(url); const { get, set } = useCache(); }
function UserProfile() { const { data: user } = useFetch('/api/user'); const { cache } = useCache(); }
|
最佳实践:
- 遵循”最小可用原则”:先简单实现,有复用需求再抽象
- 分层设计:底层通用 Hook + 中层组合 Hook + 高层业务 Hook
- 避免”万能 Hook”:一个 Hook 不应该有超过 5 个参数或 10 个返回值
6.4 性能陷阱
问题描述: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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| function useUserData() { const [user, setUser] = useState(null); return { user, updateName: (name: string) => setUser({ ...user, name }), updateAge: (age: number) => setUser({ ...user, age }), }; }
const UserProfile = memo(({ updateName }) => { });
function useUserData() { const [user, setUser] = useState(null); const updateName = useCallback((name: string) => { setUser(prev => ({ ...prev, name })); }, []); const updateAge = useCallback((age: number) => { setUser(prev => ({ ...prev, age })); }, []); return useMemo( () => ({ user, updateName, updateAge }), [user, updateName, updateAge] ); }
function useExpensiveComputation(data: any[]) { const result = useMemo(() => { return data.map(item => { return complexTransform(item); }); }, [data]); return result; }
function useExpensiveComputation(data: any[]) { const dataIds = data.map(d => d.id).join(','); const result = useMemo(() => { return data.map(item => complexTransform(item)); }, [dataIds]); return result; }
function BadComponent() { const [a, setA] = useState(0); const [b, setB] = useState(0); const [c, setC] = useState(0); useEffect(() => { setB(a * 2); }, [a]); useEffect(() => { setC(b + 10); }, [b]); useEffect(() => { doSomething(c); }, [c]); }
function GoodComponent() { const [a, setA] = useState(0); const b = a * 2; const c = b + 10; useEffect(() => { doSomething(c); }, [c]); }
|
最佳实践:
- 返回的函数/对象使用
useCallback/useMemo 缓存
- 优先使用派生状态(普通变量)而不是
useEffect + setState
- 使用 React DevTools Profiler 检测性能问题
- 对于复杂状态逻辑考虑使用
useReducer
6.5 测试陷阱
问题描述: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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| function useBadAuth() { const [user, setUser] = useState(null); useEffect(() => { fetch('/api/auth/me') .then(res => res.json()) .then(setUser); }, []); const login = async (credentials: any) => { const res = await fetch('/api/auth/login', { method: 'POST', body: JSON.stringify(credentials), }); const user = await res.json(); setUser(user); }; return { user, login }; }
function useGoodAuth(authService = defaultAuthService) { const [user, setUser] = useState(null); useEffect(() => { authService.getMe().then(setUser); }, [authService]); const login = useCallback(async (credentials: any) => { const user = await authService.login(credentials); setUser(user); }, [authService]); return { user, login }; }
test('useAuth login', async () => { const mockService = { getMe: jest.fn(), login: jest.fn().mockResolvedValue({ id: 1, name: 'Test' }), }; const { result } = renderHook(() => useGoodAuth(mockService)); await act(async () => { await result.current.login({ username: 'test' }); }); expect(result.current.user).toEqual({ id: 1, name: 'Test' }); });
|
最佳实践:
- 使用依赖注入,避免直接依赖全局变量
- 将副作用隔离到独立的函数中
- 使用
@testing-library/react-hooks 测试 Hook
- 对于复杂 Hook,编写集成测试而不是单元测试
7. 总结:Hook 设计 = 架构设计的微缩版
7.1 核心思想对照
| 软件架构原则 |
Hook 设计体现 |
| 单一职责原则 (SRP) |
每个 Hook 只做一件事 |
| 开闭原则 (OCP) |
通过组合扩展,而不是修改 Hook 内部 |
| 依赖倒置原则 (DIP) |
依赖注入,而不是硬编码依赖 |
| 接口隔离原则 (ISP) |
返回最小必要接口,不返回无关数据 |
| 最少知识原则 |
Hook 不应该”伸手”修改外部状态 |
7.2 设计检查清单
在完成一个自定义 Hook 后,问自己这些问题:
7.3 从 Hook 设计到系统设计
优秀的 Hook 设计者往往也是优秀的系统架构师,因为他们理解:
- 抽象的价值:不是为了少写代码,而是为了隔离关注点
- 边界的重要性:清晰的边界让系统更易维护
- 组合的力量:小而专注的模块通过组合产生强大功能
- 权衡的艺术:没有完美的设计,只有适合场景的设计
最后的建议:
- 先在组件中实现功能,确认逻辑可行后再抽 Hook
- 遵循”Rule of Three”:同样的逻辑出现 3 次再考虑抽象
- 优先组合而非继承:使用小 Hook 组合,而非大而全的 Hook
- 持续重构:随着理解深入,不断优化 Hook 设计
参考资源