第六节:React Hooks进阶篇-自定义Hook设计
实战题:实现一个useWindowSize或useFetch
自定义 Hook 设计实战:useWindowSize
与 useFetch
实现详解
一、useWindowSize
:实时监听窗口尺寸
1. 基础实现(TypeScript 版)
import { useState, useEffect } from 'react';// 定义返回值的类型
type WindowSize = {width: number;height: number;
};const useWindowSize = (): WindowSize => {// 处理 SSR 环境(如 Next.js)const isSSR = typeof window === 'undefined';// 初始化状态(兼容 SSR)const [windowSize, setWindowSize] = useState<WindowSize>(() => ({width: isSSR ? 0 : window.innerWidth,height: isSSR ? 0 : window.innerHeight,}));useEffect(() => {if (isSSR) return; // 服务端无需执行const handleResize = () => {setWindowSize({width: window.innerWidth,height: window.innerHeight,});};// 立即触发一次以获取初始值handleResize();// 添加事件监听window.addEventListener('resize', handleResize);// 清理函数:移除监听return () => {window.removeEventListener('resize', handleResize);};}, [isSSR]); // 依赖项为 isSSR(仅挂载时执行)return windowSize;
};// 使用示例
const ResponsiveComponent = () => {const { width, height } = useWindowSize();return <div>当前窗口尺寸:{width}px × {height}px</div>;
};
2. 进阶优化:防抖处理
// 防抖工具函数
const debounce = (fn: Function, delay: number) => {let timer: NodeJS.Timeout;return (...args: any[]) => {clearTimeout(timer);timer = setTimeout(() => fn(...args), delay);};
};// 修改 useEffect 部分
useEffect(() => {// ...其他代码同前const debouncedResize = debounce(handleResize, 200);window.addEventListener('resize', debouncedResize);return () => window.removeEventListener('resize', debouncedResize);
}, [isSSR]);
二、useFetch
:通用数据请求 Hook
1. 完整实现(支持 TypeScript 泛型)
import { useState, useEffect } from 'react';// 定义状态类型
type FetchState<T> = {data: T | null;isLoading: boolean;error: Error | null;
};// 请求配置类型
type FetchOptions = RequestInit & {timeout?: number;
};const useFetch = <T,>(url: string,options?: FetchOptions
): FetchState<T> => {const [state, setState] = useState<FetchState<T>>({data: null,isLoading: true,error: null,});useEffect(() => {const controller = new AbortController();const { signal } = controller;let timeoutId: NodeJS.Timeout;const fetchData = async () => {try {// 设置超时if (options?.timeout) {timeoutId = setTimeout(() => {controller.abort();setState({data: null,isLoading: false,error: new Error('请求超时'),});}, options.timeout);}const response = await fetch(url, {...options,signal,});if (!response.ok) {throw new Error(`HTTP 错误 ${response.status}`);}const data: T = await response.json();setState({ data, isLoading: false, error: null });} catch (error) {// 忽略已取消的请求错误if (error.name !== 'AbortError') {setState({data: null,isLoading: false,error: error as Error,});}} finally {clearTimeout(timeoutId);}};fetchData();// 清理函数:取消请求return () => {controller.abort();clearTimeout(timeoutId);};}, [url, options?.timeout]); // 依赖项:URL 和超时时间return state;
};// 使用示例
interface User {id: number;name: string;
}const UserList = () => {const { data, isLoading, error } = useFetch<User[]>('/api/users', {timeout: 5000,});if (isLoading) return <div>加载中...</div>;if (error) return <div>错误:{error.message}</div>;return (<ul>{data?.map((user) => (<li key={user.id}>{user.name}</li>))}</ul>);
};
2. 关键功能解析
特性 | 实现方式 |
---|---|
请求取消 | AbortController + signal 参数 |
超时处理 | setTimeout + 自动取消请求 |
类型安全 | 泛型 <T> 定义响应数据类型 |
错误处理 | 区分网络错误、HTTP 状态码错误、超时错误 |
SSR 兼容 | 无需特殊处理(浏览器 API 在服务端不会执行) |
三、设计原则与最佳实践
1. 通用 Hook 设计规范
• 单一职责
• useWindowSize
只关注窗口尺寸变化
• useFetch
仅处理数据请求生命周期
• 明确输入输出
// useWindowSize:无需参数,返回尺寸对象
const { width } = useWindowSize();// useFetch:接收 URL 和配置,返回状态对象
const { data } = useFetch<User>('/api/user');
• 副作用清理
// 组件卸载时移除监听器/取消请求
useEffect(() => () => {window.removeEventListener('resize', handleResize);controller.abort();
}, []);
2. 性能优化策略
• 防抖/节流
// 避免 resize 事件高频触发状态更新
const debouncedResize = debounce(handleResize, 200);
• 条件执行
// 仅在 URL 变化时发起新请求
useEffect(() => {if (!url) return;fetchData();
}, [url]);
• 记忆化配置
// 使用 useMemo 避免重复触发请求
const options = useMemo(() => ({ timeout: 5000 }), []);
const { data } = useFetch(url, options);
四、对比总结
Hook | 核心场景 | 关键技术点 | 优化方向 |
---|---|---|---|
useWindowSize | 响应式布局、图表自适应 | 事件监听、SSR 兼容、防抖 | 动态阈值判断(如移动端横竖屏) |
useFetch | 数据列表加载、表单提交 | 请求取消、错误处理、泛型支持 | 缓存策略(SWR/React Query) |
五、实战技巧
-
组合使用 Hooks
// 响应式布局 + 数据加载 const { width } = useWindowSize(); const isMobile = width < 768; const { data } = useFetch<Article[]>(isMobile ? '/mobile-api' : '/desktop-api');
-
扩展
useFetch
• 添加重试机制
• 支持 JWT 自动刷新
• 集成全局 Loading 状态管理 -
测试策略
• 使用 Jest 模拟window.resize
事件
• 利用msw
模拟 API 请求
通过合理设计自定义 Hook,可以显著提升代码复用率与可维护性。建议将通用逻辑抽象为 Hook,并通过文档明确使用约束。