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

组件卸载后如何持续调用useQuery的refetch完成文件下载?

文件下载流程在组件卸载后中断的解决方案

问题背景

用TanStack Query的useQuery实现两步文件下载:

  1. 调用API1轮询检查文件是否就绪,就绪后触发API2下载
  2. 要求用户导航离开页面后,下载流程仍能继续执行
    但当前实现中,下载模态框组件卸载后,hook里的API1轮询会停止,导致无法触发后续下载。

可行解决方案

1. 开启TanStack Query的后台轮询

TanStack Query本身支持后台轮询,不需要额外依赖。直接给API1的useQuery配置以下参数:

// useDownloadBegin内部的useQuery配置
useQuery({
  queryKey: ['checkFileStatus', fileId], // 用唯一标识确保查询实例唯一
  queryFn: () => fetchAPI1(),
  refetchInterval: 3000, // 每3秒轮询一次
  refetchIntervalInBackground: true, // 允许后台(组件卸载后)继续轮询
  staleTime: Infinity, // 避免缓存干扰轮询结果
  onSuccess: (data) => {
    if (data.status === 'Ready for download') {
      // 文件就绪后触发API2下载
      triggerDownloadFile();
      // 停止轮询
      queryClient.cancelQueries({ queryKey: ['checkFileStatus', fileId] });
    }
  },
  enabled: true, // 确保查询始终处于活跃状态
})

这样即使组件卸载,TanStack Query的查询实例会在后台保持轮询,直到文件就绪后触发下载。

2. 将下载逻辑提升到全局Context

把下载逻辑从组件级的hook迁移到全局Context中,确保逻辑不受组件生命周期影响:

  1. 创建全局的DownloadContext,Provider放在根组件(比如App.js)内
  2. 在Context的Provider中实现完整的下载逻辑(API1轮询+API2下载)
  3. 组件只需要调用Context提供的startDownload方法触发流程,不需要持有状态

示例代码:

// DownloadContext.js
import { createContext, useContext, useQueryClient } from 'react';

const DownloadContext = createContext();

export const DownloadProvider = ({ children }) => {
  const queryClient = useQueryClient();

  const startDownload = (fileId) => {
    // 启动API1轮询
    queryClient.prefetchQuery({
      queryKey: ['checkFileStatus', fileId],
      queryFn: () => fetchAPI1(fileId),
      refetchInterval: 3000,
      refetchIntervalInBackground: true,
      staleTime: Infinity,
      onSuccess: (data) => {
        if (data.status === 'Ready for download') {
          // 触发API2下载
          fetchAPI2(fileId).then((fileBlob) => {
            // 创建下载链接
            const url = URL.createObjectURL(fileBlob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `file-${fileId}.pdf`;
            a.click();
            URL.revokeObjectURL(url);
          });
          // 停止轮询
          queryClient.cancelQueries({ queryKey: ['checkFileStatus', fileId] });
        }
      },
    });
  };

  return (
    <DownloadContext.Provider value={{ startDownload }}>
      {children}
    </DownloadContext.Provider>
  );
};

export const useDownload = () => useContext(DownloadContext);

根组件引入Provider:

// App.js
import { DownloadProvider } from './DownloadContext';

function App() {
  return (
    <DownloadProvider>
      {/* 其他路由和组件 */}
    </DownloadProvider>
  );
}

下载模态框中调用:

// DownloadModal.js
import { useDownload } from './DownloadContext';

export const DownloadModal = ({ fileId }) => {
  const { startDownload } = useDownload();

  const handleStart = () => {
    startDownload(fileId);
    // 可关闭模态框或导航到其他页面
  };

  return (
    <div>
      <button onClick={handleStart}>开始下载</button>
    </div>
  );
};

3. 脱离React生命周期的全局任务管理

如果不想依赖TanStack Query的特性,可以用独立的全局任务队列管理下载流程:

// downloadManager.js
const downloadTasks = new Map();

export const startFileDownload = (fileId) => {
  if (downloadTasks.has(fileId)) return; // 避免重复任务

  const intervalId = setInterval(async () => {
    try {
      const data = await fetchAPI1(fileId);
      if (data.status === 'Ready for download') {
        // 触发下载
        const fileBlob = await fetchAPI2(fileId);
        const url = URL.createObjectURL(fileBlob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `file-${fileId}.pdf`;
        a.click();
        URL.revokeObjectURL(url);
        // 清理任务
        clearInterval(intervalId);
        downloadTasks.delete(fileId);
      }
    } catch (err) {
      console.error('下载出错:', err);
      clearInterval(intervalId);
      downloadTasks.delete(fileId);
    }
  }, 3000);

  downloadTasks.set(fileId, intervalId);
};

export const cancelFileDownload = (fileId) => {
  const intervalId = downloadTasks.get(fileId);
  if (intervalId) {
    clearInterval(intervalId);
    downloadTasks.delete(fileId);
  }
};

组件中调用:

// DownloadModal.js
import { startFileDownload } from './downloadManager';

export const DownloadModal = ({ fileId }) => {
  const handleStart = () => {
    startFileDownload(fileId);
  };

  return (
    <div>
      <button onClick={handleStart}>开始下载</button>
    </div>
  );
};

这种方式完全脱离React组件生命周期,不管组件是否卸载,轮询和下载都会继续执行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 14:27:44