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

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的重复触发问题:

  1. 父组件中创建全局的上传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>
  );
}
  1. 子组件通过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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 13:15:51