深入React自定义Hooks:从基础实践到高级封装
在react开发中,随着业务复杂度提升,仅依靠内置Hooks往往难以满足需求。本文将系统性地介绍如何设计和封装高质量的自定义Hooks,帮助你提升代码复用性和开发效率。
一、自定义Hooks基础概念
1.1 什么是自定义Hooks
自定义Hooks本质上是一种逻辑复用机制,它允许你将组件逻辑提取到可重用的函数中。与工具函数不同,自定义Hooks:
可以调用其他Hooks
遵循use前缀命名约定
保持React的声明式特性
1.2 设计原则
单一职责:每个Hook应只解决一个特定问题
明确依赖:清晰定义输入输出接口
性能优化:合理使用useMemo/useCallback避免不必要的计算
类型安全:为TypeScript项目提供完整的类型定义
二、实战:从简单到复杂的Hook封装
2.1 事件监听Hook优化版
原始版本存在handler更新不及时的问题,以下是改进实现:
import { useEffect, useRef } from 'react';
const useEventListener = <K extends keyof WindowEventMap>(
eventType: K,
handler: (event: WindowEventMap[K]) => void,
element: Window | htmlElement = window,
options?: boolean | AddEventListenerOptions
) => {
const savedHandler = useRef(handler);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const isSupported = element && element.addEventListener;
if (!isSupported) return;
const eventListener = (event: WindowEventMap[K]) => savedHandler.current(event);
element.addEventListener(eventType, eventListener, options);
return () => {
element.removeEventListener(eventType, eventListener, options);
};
}, [eventType, element, options]);
};
// 使用示例
const WindowResizeDemo = () => {
useEventListener('resize', (e) => {
console.log('Window size:', window.innerWidth, window.innerHeight);
});
return <div>调整浏览器窗口大小查看控制台输出</div>;
};关键改进:
完整的TypeScript类型支持
支持更多事件目标元素
完善的options参数传递
添加了api可用性检查
2.2 增强版定时器Hook
针对复杂定时器场景,我们设计更强大的useInterval:
import { useEffect, useRef } from 'react';
const useAdvancedInterval = (
callback: () => void,
delay: number | null,
options?: {
immediate?: boolean; // 是否立即执行
maxTimes?: number; // 最大执行次数
onEnd?: () => void; // 结束时回调
}
) => {
const savedCallback = useRef(callback);
const timesRef = useRef(0);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
let id: NodeJS.Timeout;
const tick = () => {
savedCallback.current();
timesRef.current += 1;
if (options?.maxTimes && timesRef.current >= options.maxTimes) {
clearInterval(id);
options.onEnd?.();
}
};
if (options?.immediate) {
tick(); // 立即执行一次
id = setInterval(tick, delay);
} else {
id = setInterval(tick, delay);
}
return () => {
clearInterval(id);
timesRef.current = 0;
};
}, [delay, options?.immediate, options?.maxTimes, options?.onEnd]);
};
// 九宫格抽奖示例
const LotteryGrid = () => {
const [position, setPosition] = useState(0);
const [speed, setSpeed] = useState(500);
const [isWinning, setIsWinning] = useState(false);
useAdvancedInterval(
() => {
setPosition((prev) => (prev + 1) % 8);
// 加速逻辑
if (!isWinning && speed > 100) {
setSpeed(s => s - 50);
}
},
isWinning ? null : speed,
{ maxTimes: 20 }
);
// ...其他抽奖逻辑
};高级特性:
支持立即执行模式
可设置最大执行次数
结束回调通知
动态调整间隔时间
三、数据请求Hook的工业级实现
3.1 完整版useRequest实现
import { useState, useEffect, useCallback, useRef } from 'react';
type RequestOptions<T> = {
manual?: boolean;
defaultParams?: any[];
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
formatResult?: (response: any) => T;
};
const useRequest = <T>(
service: (...args: any[]) => Promise<T>,
options: RequestOptions<T> = {}
) => {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);
const paramsRef = useRef(options.defaultParams || []);
const run = useCallback(async (...args: any[]) => {
paramsRef.current = args;
setLoading(true);
try {
let response = await service(...args);
if (options.formatResult) {
response = options.formatResult(response);
}
setData(response);
options.onSuccess?.(response);
} catch (err) {
setError(err as Error);
options.onError?.(err as Error);
} finally {
setLoading(false);
}
}, [service, options.formatResult, options.onSuccess, options.onError]);
const refresh = useCallback(() => {
return run(...paramsRef.current);
}, [run]);
useEffect(() => {
if (!options.manual) {
run(...(options.defaultParams || []));
}
}, [run, options.manual, options.defaultParams]);
return {
data,
error,
loading,
run,
refresh,
mutate: setData, // 直接修改data
};
};
// 使用示例
const UserInfo = ({ userId }) => {
const { data: user, loading, error } = useRequest(
() => fetch(`/api/users/${userId}`).then(res => res.json()),
{
formatResult: (res) => ({
...res,
fullName: `${res.firstName} ${res.lastName}`,
}),
onError: (err) => {
console.error('获取用户信息失败:', err);
showToast('加载失败,请重试');
}
}
);
if (loading) return <Spinner />;
if (error) return <ErrorDisplay />;
return (
<div>
<h1>{user?.fullName}</h1>
{/* 其他用户信息 */}
</div>
);
};核心功能:
自动/手动触发模式
结果格式化
生命周期钩子
数据突变能力
请求参数记忆
3.2 开源方案对比
| 特性 | 自制useRequest | SWR | React Query | ahooks |
|---|---|---|---|---|
| 自动缓存 | ❌ | ✅ | ✅ | ✅ |
| 请求去重 | ❌ | ✅ | ✅ | ✅ |
| 分页支持 | ❌ | ✅ | ✅ | ✅ |
| 乐观更新 | ❌ | ✅ | ✅ | ✅ |
| 依赖请求 | ❌ | ✅ | ✅ | ✅ |
| 轻量级 | ✅ | ✅ | ❌ | ✅ |
| 学习曲线 | 低 | 中 | 高 | 中 |
选型建议:
简单项目:自制Hook或ahooks
数据密集型:React Query
需要轻量方案:SWR
四、高级Hook模式
4.1 状态机Hook
const useStateMachine = <T extends string>(
initialState: T,
transitions: Record<T, Array<T>> // 状态转移规则
) => {
const [state, setState] = useState(initialState);
const transition = useCallback((newState: T) => {
if (transitions[state].includes(newState)) {
setState(newState);
} else {
console.warn(`Invalid transition from ${state} to ${newState}`);
}
}, [state]);
return [state, transition] as const;
};
// 使用示例
const TrafficLight = () => {
const [light, changeLight] = useStateMachine('red', {
red: ['green'],
green: ['yellow'],
yellow: ['red'],
// 红灯不能直接变黄灯
});
return (
<div>
<div style={{ color: light }}>当前: {light}</div>
<button onClick={() => changeLight('green')}>变绿灯</button>
{/* 其他按钮 */}
</div>
);
};4.2 防抖/节流Hook
const useDebounce = <T extends any[]>(
callback: (...args: T) => void,
delay: number
) => {
const timerRef = useRef<NodeJS.Timeout>();
useEffect(() => {
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, []);
return useCallback((...args: T) => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
timerRef.current = setTimeout(() => {
callback(...args);
}, delay);
}, [callback, delay]);
};
// 使用示例
const SearchBox = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleSearch = useDebounce(async (value) => {
const res = await fetch(`/api/search?q=${value}`);
setResults(await res.json());
}, 500);
return (
<div>
<input
value={query}
onChange={(e) => {
setQuery(e.target.value);
handleSearch(e.target.value);
}}
/>
{/* 显示结果 */}
</div>
);
};五、测试与调试自定义Hooks
5.1 测试策略
import { renderHook, act } from '@testing-library/react-hooks';
import { useCounter } from './useCounter';
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});5.2 调试技巧
使用useDebugValue展示Hook内部状态
const useToggle = (initialState = false) => {
const [state, setState] = useState(initialState);
useDebugValue(state ? 'On' : 'Off');
// ...
};React DevTools查看Hook依赖关系
六、Hook集合推荐
ahooks (阿里出品)
useRequest: 强大的异步管理
useAntdTable: 与Ant Design深度集成
useDrag/useDrop: 拖拽功能
react-use (社区流行)
usePrevious: 获取之前的值
useClickAway: 点击外部区域触发
useLocalStorage: 本地存储同步
Redux Toolkit Hooks
useSelector/useDispatch: Redux集成
useStore: 访问整个store
七、挑战与解决方案
挑战1:Hook执行顺序必须一致
解决方案:不要在条件/循环中使用Hook
错误示例:
if (condition) { useEffect(() => {...}); // ❌ }
挑战2:闭包陷阱
解决方案:使用ref保存最新值或使用函数式更新
const [count, setCount] = useState(0); // 错误方式 useEffect(() => { const timer = setInterval(() => { setCount(count + 1); // 闭包问题 }, 1000); return () => clearInterval(timer); }, []); // 正确方式 useEffect(() => { const timer = setInterval(() => { setCount(prev => prev + 1); // 函数式更新 }, 1000); return () => clearInterval(timer); }, []);
挑战3:性能优化
解决方案:合理使用useMemo/useCallback
const expensiveValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); const stableCallback = useCallback(() => doSomething(a, b), [a, b]);
八、练习:实现useSwitch
const useSwitch = (initialValue = false) => {
const [state, setState] = useState(initialValue);
const toggle = useCallback(() => {
setState(prev => !prev);
}, []);
return [state, toggle] as const;
};
// 使用示例
const ToggleButton = () => {
const [on, toggle] = useSwitch(false);
return (
<button onClick={toggle}>
{on ? 'ON' : 'OFF'}
</button>
);
};通过本文的学习,你应该已经掌握了自定义Hooks从设计到实现的完整知识体系。记住,好的Hook应该像乐高积木一样,既独立完整又能与其他部分完美配合。在实际项目中,不断提炼和优化你的Hooks,它们将成为你React开发中的强大武器。
本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!