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

如何基于react-query实现并发查询限制的自定义Hook?

解决方案:静态文档的并发获取与缓存管理

你的核心实现思路完全正确:先拉取文档元数据列表、再分批并发获取文档内容,结合staleTime: Infinity利用缓存避免重复请求,是处理静态未知数量文档场景的合理方案。以下是具体的落地实现细节:

一、核心思路验证

  1. 先获取元数据再拉取文档:必须先拿到文档的ID、类型等元数据,才能针对性发起内容请求,这个流程逻辑没问题。
  2. staleTime: Infinity的使用:对于不会变更的静态文档,这个配置能让TanStack Query永久缓存请求结果,除非手动调用invalidateQueries,完全符合你的需求。
  3. 自定义Hook控制并发:未知数量的请求如果一次性发起,会造成服务器压力或浏览器性能问题,通过自定义Hook做并发限制是必要的。

二、具体实现方案(基于TanStack Query)

1. 文档元数据列表查询

先实现获取元数据的Hook,同样设置staleTime: Infinity确保元数据也被永久缓存:

const useDocumentList = () => {
  return useQuery({
    queryKey: ['document-list'],
    queryFn: async () => {
      // 替换为你的元数据请求接口
      const res = await fetch('/api/documents/metadata');
      return res.json();
    },
    staleTime: Infinity,
  });
};

2. 并发限制的文档内容获取Hook

这个Hook会自动过滤已缓存的文档,管理请求队列,控制同时进行的请求数量:

import { useState, useEffect, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';

const useConcurrentDocuments = (documentIds: string[], concurrency = 3) => {
  const queryClient = useQueryClient();
  const [activeRequests, setActiveRequests] = useState<Set<string>>(new Set());
  const [queuedIds, setQueuedIds] = useState<string[]>([]);
  const [fetchedDocs, setFetchedDocs] = useState<Record<string, Blob>>({});

  // 初始化队列:排除已缓存的文档
  useEffect(() => {
    const uncachedIds = documentIds.filter(id => !queryClient.getQueryData(['document', id]));
    setQueuedIds(uncachedIds);
  }, [documentIds, queryClient]);

  // 处理队列中的请求,控制并发数
  useEffect(() => {
    if (queuedIds.length === 0) return;

    // 计算还能发起的请求数
    const availableSlots = concurrency - activeRequests.size;
    if (availableSlots <= 0) return;

    // 取出队列中对应数量的文档ID
    const nextBatch = queuedIds.slice(0, availableSlots);
    nextBatch.forEach(docId => {
      setActiveRequests(prev => new Set([...prev, docId]));
      
      // 用fetchQuery发起请求并缓存
      queryClient.fetchQuery({
        queryKey: ['document', docId],
        queryFn: async () => {
          const res = await fetch(`/api/documents/${docId}`);
          return res.blob();
        },
        staleTime: Infinity,
      })
      .then(blob => {
        setFetchedDocs(prev => ({ ...prev, [docId]: blob }));
      })
      .catch(err => {
        console.error(`获取文档${docId}失败:`, err);
      })
      .finally(() => {
        // 完成后从活跃集合和队列中移除
        setActiveRequests(prev => {
          const newSet = new Set(prev);
          newSet.delete(docId);
          return newSet;
        });
        setQueuedIds(prev => prev.filter(id => id !== docId));
      });
    });
  }, [queuedIds, activeRequests, concurrency, queryClient]);

  // 合并缓存和已获取的文档
  const allDocuments = useMemo(() => {
    return documentIds.reduce((acc, id) => {
      const cachedDoc = queryClient.getQueryData(['document', id]);
      return { ...acc, [id]: cachedDoc || fetchedDocs[id] };
    }, {} as Record<string, Blob | undefined>);
  }, [documentIds, fetchedDocs, queryClient]);

  const isLoading = queuedIds.length > 0 || activeRequests.size > 0;

  return { documents: allDocuments, isLoading };
};

3. 页面组件中组合使用

将两个Hook结合,渲染不同类型的文档:

const DocumentGallery = () => {
  const { data: documentMetadata, isLoading: loadingMetadata } = useDocumentList();
  const docIds = documentMetadata?.map(doc => doc.id) || [];
  const { documents, isLoading: loadingDocs } = useConcurrentDocuments(docIds, 3);

  if (loadingMetadata || loadingDocs) {
    return <div>加载中...</div>;
  }

  return (
    <div className="document-grid">
      {documentMetadata?.map(doc => {
        const blob = documents[doc.id];
        if (!blob) return null;

        const objectUrl = URL.createObjectURL(blob);
        
        // 根据文档类型渲染
        if (doc.mimeType.startsWith('image/')) {
          return (
            <img
              key={doc.id}
              src={objectUrl}
              alt={doc.name}
              onLoad={() => URL.revokeObjectURL(objectUrl)}
              className="document-item"
            />
          );
        } else if (doc.mimeType === 'application/pdf') {
          return (
            <iframe
              key={doc.id}
              src={objectUrl}
              title={doc.name}
              onLoad={() => URL.revokeObjectURL(objectUrl)}
              className="document-item pdf-viewer"
            />
          );
        }

        return (
          <div key={doc.id} className="document-item">
            {doc.name}(不支持的文件类型)
          </div>
        );
      })}
    </div>
  );
};

三、关键注意事项

  1. 缓存复用:每次发起请求前先通过queryClient.getQueryData检查缓存,确保已获取过的文档不会重复请求。
  2. 内存泄漏处理:使用URL.createObjectURL生成的链接必须在组件卸载或资源加载完成后调用URL.revokeObjectURL释放内存。
  3. 错误隔离:单个文档请求失败不影响其他请求的执行,可根据需求添加重试逻辑(比如在fetchQuery中配置retry参数)。
  4. 并发数调整:根据服务器承受能力和浏览器限制,合理设置concurrency参数(一般3-5个较为合适)。

内容的提问来源于stack exchange,提问作者ToM

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 00:33:16