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
相关产品推荐
相关产品推荐

