You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 01:45:43