题目
实现一个带缓存、防抖和错误处理的自定义数据请求Hook
信息
- 类型:问答
- 难度:⭐⭐
考点
useState, useEffect, 自定义Hook设计, 异步处理, 性能优化
快速回答
实现一个自定义Hook useFetch 需满足:
- 支持GET请求数据并缓存结果
- 输入参数变化时自动重新请求
- 实现500ms防抖避免频繁请求
- 返回包含数据/加载状态/错误的对象
核心实现要点:
- 使用
useState管理数据/加载/错误状态 - 用
useEffect处理异步请求和清理 - 通过
AbortController取消未完成请求 - 使用
useRef存储缓存和防抖计时器
需求分析
需要创建一个可复用的自定义Hook,用于处理数据请求场景,需满足:
- 缓存机制:相同URL请求直接返回缓存数据
- 防抖控制:URL频繁变化时延迟500ms再发起请求
- 错误处理:捕获网络错误并更新状态
- 竞态处理:确保返回最新请求结果
代码实现
import { useState, useEffect, useRef } from 'react';
const CACHE = {};
export function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// 使用ref存储防抖计时器和最新请求标识
const debounceRef = useRef(null);
const abortRef = useRef(null);
useEffect(() => {
// 防抖处理:清除前一个计时器
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
// 空URL处理
if (!url) {
setData(null);
return;
}
// 检查缓存
if (CACHE[url]) {
setData(CACHE[url]);
return;
}
// 设置防抖延迟请求
debounceRef.current = setTimeout(async () => {
try {
// 取消未完成的请求
if (abortRef.current) {
abortRef.current.abort();
}
abortRef.current = new AbortController();
setLoading(true);
setError(null);
const response = await fetch(url, {
signal: abortRef.current.signal
});
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`);
}
const result = await response.json();
// 更新缓存和状态
CACHE[url] = result;
setData(result);
} catch (err) {
// 忽略被取消的请求错误
if (err.name !== 'AbortError') {
setError(err.message);
setData(null);
}
} finally {
setLoading(false);
abortRef.current = null;
}
}, 500);
// 清理函数:取消请求和计时器
return () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
if (abortRef.current) {
abortRef.current.abort();
}
};
}, [url]);
return { data, loading, error };
}原理说明
- 缓存机制:使用模块级变量存储缓存,相同URL直接返回数据
- 防抖实现:通过
setTimeout和clearTimeout控制请求触发频率 - 竞态处理:
AbortController确保总是获取最新请求结果 - 状态管理:三组state分别跟踪数据/加载/错误状态
最佳实践
- 缓存策略:可扩展为带过期时间的缓存(如
CACHE[url] = { data, timestamp }) - 参数处理:支持POST请求时可增加
options参数 - 错误分类:根据HTTP状态码返回更具体的错误信息
- 内存管理:大型应用建议使用LRU缓存策略
常见错误
- 未处理组件卸载:未在清理函数中取消请求会导致
Can't perform state update on unmounted component警告 - 缺少竞态处理:连续快速切换参数时可能返回旧请求结果
- 缓存滥用:对动态数据接口使用缓存导致数据过期
- 防抖失效:未正确清除前一个计时器导致多次请求
扩展知识
- SWR/React Query:推荐使用成熟的数据请求库处理复杂场景
- useReducer:当状态逻辑复杂时可替代多个
useState - TypeScript集成:为Hook添加泛型支持
useFetch<TData>(url) - 性能优化:结合
useCallback记忆化回调函数