如何使用react-query维护缓存数据池实现差量数据拉取
带缓存增量拉取的乐观更新自定义Hook实现
核心实现思路
- 用独立
Map结构维护缓存,以数据ID为键存储对应内容,读写复杂度O(1),支持跨组件复用 - 每次收到待请求的ID集合,立刻拆分缓存命中项和待拉取新项,优先把命中的缓存数据同步返回,实现无等待的乐观渲染
- 仅对不在缓存/缓存已过期的ID发起接口请求,请求成功后写入缓存,再增量更新返回结果,不重复拉取已有数据
- 增加请求中标记,避免同一ID短时间内被重复发起请求
可直接复用的代码实现
import { useState, useEffect, useRef, useCallback } from 'react'; // 全局共享缓存,如需组件实例级隔离可移到Hook内部 const globalCache = new Map<string | number, any>(); // 记录正在请求中的ID,防止重复发起请求 const pendingRequestIds = new Set<string | number>(); type UseIncrementalCacheOptions<T> = { // 业务侧传入的数据拉取方法,接收ID数组返回ID到数据的映射 fetcher: (ids: (string | number)[]) => Promise<Record<string | number, T>>; // 可选配置:缓存过期时间,单位毫秒,默认永不过期 cacheExpireTime?: number; }; export default function useIncrementalCache<T = any>( requestIds: (string | number)[], { fetcher, cacheExpireTime = 0 }: UseIncrementalCacheOptions<T> ) { const [dataMap, setDataMap] = useState<Record<string | number, T>>({}); const [loading, setLoading] = useState(false); const [error, setError] = useState<Error | null>(null); // 记录每个缓存项的过期时间戳 const cacheExpireMapRef = useRef(new Map<string | number, number>()); const fetchMissingData = useCallback(async (missingIds: (string | number)[]) => { if (missingIds.length === 0) return; // 过滤掉已经在请求中的ID const needFetchIds = missingIds.filter(id => !pendingRequestIds.has(id)); if (needFetchIds.length === 0) return; needFetchIds.forEach(id => pendingRequestIds.add(id)); setLoading(true); setError(null); try { const newData = await fetcher(needFetchIds); // 新数据写入缓存 Object.entries(newData).forEach(([id, item]) => { const parseId = isNaN(Number(id)) ? id : Number(id); globalCache.set(parseId, item); if (cacheExpireTime > 0) { cacheExpireMapRef.current.set(parseId, Date.now() + cacheExpireTime); } }); // 增量更新返回数据,不覆盖已命中的缓存内容 setDataMap(prev => ({ ...prev, ...newData })); } catch (err) { setError(err as Error); } finally { needFetchIds.forEach(id => pendingRequestIds.delete(id)); setLoading(false); } }, [fetcher, cacheExpireTime]); useEffect(() => { if (!requestIds?.length) { setDataMap({}); return; } const hitData: Record<string | number, T> = {}; const missingIds: (string | number)[] = []; requestIds.forEach(id => { const cachedItem = globalCache.get(id); const expireTime = cacheExpireMapRef.current.get(id) || 0; // 命中未过期缓存直接收集,否则加入待拉取列表 if (cachedItem !== undefined && (cacheExpireTime === 0 || Date.now() < expireTime)) { hitData[id] = cachedItem; } else { missingIds.push(id); } }); // 乐观更新:第一时间返回缓存命中内容,无需等待接口 setDataMap(hitData); // 后台拉取缺失的新数据 fetchMissingData(missingIds); }, [requestIds, fetchMissingData, cacheExpireTime]); // 提供手动清除缓存方法 const clearCache = useCallback((ids?: (string | number)[]) => { if (ids) { ids.forEach(id => { globalCache.delete(id); cacheExpireMapRef.current.delete(id); }); } else { globalCache.clear(); cacheExpireMapRef.current.clear(); } }, []); return { data: Object.values(dataMap), dataMap, loading, error, clearCache }; }
预期行为匹配验证
- 初始传入
[1,2,3,4,5]时缓存为空,所有ID进入待拉取列表,请求完成后全量写入缓存并返回 - 后续传入
[1,2,3]时所有ID均命中有效缓存,待拉取列表为空,不会发起请求,直接同步返回缓存数据 - 后续传入
[4,5,6]时4、5命中缓存先同步返回,仅6进入待拉取队列,请求完成后6的数据写入缓存,增量合并到返回结果中
乐观更新特性
- 无加载白屏:缓存命中内容在ID集合变更的当次渲染就会返回,用户无需等待接口加载
- 静默补全:缺失数据在后台拉取,拉取成功后自动更新视图,用户无感知
- 性能优化:从请求拦截、缓存复用两个维度减少不必要的接口调用,大列表场景下性能提升明显
内容的提问来源于stack exchange,提问作者Sennen Randika
相关产品推荐
相关产品推荐

