React严格并发模式下,用TanStack React Query实现多文件并行上传的规范方案
符合React Query规范的多文件并行上传实现方案
给定用户输入的文件数组,需要并行发起所有文件上传,每个文件对应一个展示上传状态的组件。原方案使用useMutation配合useEffect触发上传,但在React严格/并发模式下,useEffect会触发两次导致重复上传。现有两种备选方案(useRef控制单次触发、用useQuery替代useMutation)均觉不够正统,寻求规范实现方式。
原代码:
async function uploadFile(file) { ... } // 预期在挂载时启动文件上传 function UploadingFile({file, ...}) { const { mutate } = useMutation(() => uploadFile(file), ...); // 该写法在React严格/并发模式下无法正常工作 useEffect(() => { mutate(); }, [mutate]); } function UploadingFileList({files, ...}) { return ( <div> {files.map((file, index) => ( <UploadingFile key={index} file={file} /> ))}; </div> ); }
方案一:让上传操作幂等 + 子组件内安全触发上传
React严格模式下的双重渲染是开发环境的检测机制,目的是让开发者写出可重复执行的副作用。最规范的做法是确保uploadFile函数幂等——即多次调用也不会导致重复上传(比如后端对重复上传请求做幂等处理,或前端给每个文件标记上传状态)。
如果无法修改后端或上传逻辑,可在子组件中用useRef标记是否已触发过上传,这并非hack,而是官方推荐的开发环境下规避重复执行的方式:
function UploadingFile({file}) { const { mutate } = useMutation(() => uploadFile(file), { // 配置onSuccess/onError等回调 }); const hasInitiatedUpload = useRef(false); useEffect(() => { if (!hasInitiatedUpload.current) { mutate(); hasInitiatedUpload.current = true; } }, [mutate]); // 渲染上传状态(进度、结果等) return <div>{file.name} 上传中...</div>; }
生产环境下严格模式的双重渲染会被禁用,因此不会有重复上传的问题。
方案二:父组件统一触发上传,子组件仅负责展示状态
将上传触发逻辑集中在父组件,通过React Query的queryClient跟踪每个文件的上传状态,子组件仅接收状态并渲染,从根源避免子组件useEffect的重复触发问题:
- 父组件中创建全局的上传mutation,批量触发所有文件上传,并通过文件的唯一标识生成
mutationKey:
function UploadingFileList({files}) { const queryClient = useQueryClient(); // 用文件的name、size、lastModified生成唯一mutationKey,避免同名文件冲突 const getMutationKey = (file) => ['file-upload', file.name, file.size, file.lastModified]; const { mutate } = useMutation(uploadFile, { mutationKey: getMutationKey, // 动态生成mutationKey onSuccess: (res, file) => { // 可将上传结果缓存到queryClient,供子组件读取 queryClient.setQueryData(getMutationKey(file), res); }, // 配置onError、onSettled等回调 }); const hasTriggeredUpload = useRef(false); useEffect(() => { if (!hasTriggeredUpload.current && files.length > 0) { files.forEach(file => mutate(file)); hasTriggeredUpload.current = true; } }, [files, mutate]); return ( <div> {files.map((file) => ( <UploadingFile key={getMutationKey(file).join('-')} file={file} mutationKey={getMutationKey(file)} queryClient={queryClient} /> ))} </div> ); }
- 子组件通过
queryClient获取对应文件的上传状态,仅做渲染:
function UploadingFile({file, mutationKey, queryClient}) { const mutationState = queryClient.getMutationState({ mutationKey }); const uploadResult = queryClient.getQueryData(mutationKey); if (mutationState.status === 'pending') { return <div>{file.name} 上传中...</div>; } else if (mutationState.status === 'success') { return <div>{file.name} 上传完成:{uploadResult.url}</div>; } else if (mutationState.status === 'error') { return <div>{file.name} 上传失败:{mutationState.error.message}</div>; } return null; }
这种方式更符合React Query的状态管理理念,将业务逻辑与UI渲染解耦,也便于统一管理所有上传请求。
内容的提问来源于stack exchange,提问作者Zizheng Tai
相关产品推荐
相关产品推荐

