组件卸载后如何持续调用useQuery的refetch完成文件下载?
文件下载流程在组件卸载后中断的解决方案
问题背景
用TanStack Query的useQuery实现两步文件下载:
- 调用API1轮询检查文件是否就绪,就绪后触发API2下载
- 要求用户导航离开页面后,下载流程仍能继续执行
但当前实现中,下载模态框组件卸载后,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中,确保逻辑不受组件生命周期影响:
- 创建全局的
DownloadContext,Provider放在根组件(比如App.js)内 - 在Context的Provider中实现完整的下载逻辑(API1轮询+API2下载)
- 组件只需要调用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
相关产品推荐
相关产品推荐

