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

React函数组件useCallback中使用Axios引发无限重渲染问题

问题原因

无限重渲染的核心诱因有两个,Axios报错只是刚好触发了死循环的开关:

  • 第一处致命错误是渲染ProgressBar时写了key={uuid()}:React的diff算法靠key区分组件身份,在渲染阶段调用uuid,意味着每次父组件重渲染,每个ProgressBar都会拿到一个全新的key,React会直接把所有旧ProgressBar卸载、重新挂载全新实例,本身就会造成大量无意义的重渲染。
  • 第二处问题是useCallback的依赖设置不合理:onDrop依赖了filesUploaded状态,每次filesUploaded变化就会生成新的onDrop函数,传入useDropzone后会触发拖拽区域逻辑重算。

当Axios抛错时,catch逻辑调用setError(true)触发父组件第一次重渲染:重渲染时所有ProgressBar的key更新→全部卸载重挂→重挂过程中如果触发了状态更新(比如未执行完的FileReader回调、useDropzone因onDrop更新触发的逻辑),就会再次触发父组件重渲染,进入「状态更新→重渲染→新key→重挂载→状态更新」的无限循环。注释掉Axios请求后,没有异步状态更新触发第一次重渲染,所以循环不会启动。

另外代码里还有两处会放大重渲染问题:

  • 用全局的progress/uploaded/exists/error状态给所有文件共用,多文件上传时一个文件的状态变化会导致所有进度条重渲染
  • 单独维护fileCount状态完全多余,每次渲染直接取filesUploaded.length就能拿到最新值,配套的useEffect会触发额外的重渲染
修复步骤

1. 彻底修正key的错误用法

绝对不要在渲染过程中调用随机值生成函数当key,改用稳定的唯一标识。可以在文件加入上传列表时就生成固定的uploadId,后续用这个id当key:

// 修改onloadstart里添加文件的逻辑
reader.onloadstart = () => {
  setFilesUploaded(prevFiles => {
    const exists = prevFiles.find(uploadedFile => uploadedFile.name === file.name)
    if (exists) {
      setNotifications(prevNotif => [...prevNotif, `'${file.name}' has already been uploaded.`])
      return prevFiles
    }
    // 加入列表时就生成固定id,后续作为key使用,不会随渲染变化
    return [...prevFiles, { ...file, uploadId: uuid() }]
  })
}

// 渲染时用固定的uploadId当key
{filesUploaded.map(file => {
  return (
    <ProgressBar
      key={file.uploadId}
      file={file}
      progress={progress}
      uploaded={uploaded}
      exists={exists}
      error={error}
    />
  )
})}

2. 优化useCallback依赖,避免onDrop频繁更新

把onDrop里所有读取filesUploaded的逻辑改成函数式更新,就可以把filesUploaded从useCallback依赖里移除,保证onDrop引用稳定:

const onDrop = useCallback(acceptedFiles => {
  acceptedFiles.forEach(file => {
    const reader = new FileReader()
    reader.onloadstart = () => {
      // 所有状态更新都用函数式写法拿最新值,不依赖外部状态变量
      setFilesUploaded(prevFiles => {
        const exists = prevFiles.find(uploadedFile => uploadedFile.name === file.name)
        if (exists) {
          setNotifications(prevNotif => [...prevNotif, `'${file.name}' has already been uploaded.`])
          return prevFiles
        }
        return [...prevFiles, { ...file, uploadId: uuid() }]
      })
    }
    reader.onabort = () => {
      setError(true)
      console.log('file reading was aborted')
    }
    reader.onerror = () => {
      setError(true)
      console.log('file reading has failed')
    }
    reader.onprogress = e => {
      if (e.lengthComputable) {
        setProgress((e.loaded / e.total) * 100)
      }
    }
    reader.onload = async () => {
      try {
        const res = await axios.post('/api/images', { file: reader.result })
        if (res) {
          setUploaded(true)
          setExists(res.status === 409)
        }
      } catch (err) {
        setError(true)
        console.error(err)
      }
    }
    reader.readAsArrayBuffer(file)
  })
  // 依赖数组只保留稳定的setNotifications,React的状态setter引用是固定的,不会变化
}, [setNotifications])

3. 移除冗余逻辑

删掉多余的fileCount状态和对应的useEffect,需要用到文件数量时直接取filesUploaded.length即可,减少不必要的重渲染。

4. (可选优化)拆分单文件状态

如果需要支持多文件同时上传,建议把每个文件的上传进度、错误、成功状态存在filesUploaded数组的对应项里,不要用全局状态,这样单个文件的状态变化只会触发对应进度条重渲染,不会影响其他组件。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.31 06:21:19