NextJS项目中React文件上传进度条组件未按预期频繁更新问题
问题根因
React 对 state 采用浅比较策略,你在 progressCallback 中直接修改了原有 files 数组内的对象属性,再将原数组传入 setFiles,由于数组引用没有发生变化,React 判定 state 未更新,因此不会触发组件重渲染,这也是控制台能正常打印进度但视图无变化的核心原因。
除此之外还存在两个会导致异常的潜在问题:
- 嵌套在组件内的
Thumbs函数会随父组件每次渲染重新创建,容易引发不必要的组件卸载重挂载 - useEffect 依赖
files数组,每次更新进度修改files都会重新触发上传逻辑,导致同一份文件被多次上传
修复方案
1. 修正进度回调的state更新逻辑
采用函数式更新获取最新的 state,创建新的数组和文件对象副本,保证引用发生变化触发重渲染,同时避免闭包捕获旧的 files 数组导致的异常:
const progressCallback = useCallback((progressEvent, file) => { const progress: number = Math.ceil((progressEvent.loaded as number * 100) / progressEvent.total); console.log(progress); setFiles(prevFiles => prevFiles.map(f => f.name === file.name ? {...f, progress} : f )); }, []);
2. 修复重复上传问题
新增 ref 标记上传状态,避免 useEffect 重复触发上传逻辑:
const hasUploadStarted = useRef(false); useEffect(() => { if(!files?.length || hasUploadStarted.current) return; hasUploadStarted.current = true; setUploading(true); files.forEach((file) => { CenterService.uploadFileToCenter(center._id, file, progressCallback).then(response => { const uploadedFile = response?.data?.data?.url; if (uploadedFile) { const tempFiles = center?.files ? [...center.files, uploadedFile] : [uploadedFile]; formik.setFieldValue('files', uniq(tempFiles)); } setUploading(false); }); }); }, [center._id, formik, progressCallback]);
3. 优化Thumbs组件定义
将 Thumbs 组件移到父组件外部,通过 props 传递所需参数,避免重复创建组件:
// 放在父组件外部定义 const Thumbs = ({ files, uploading, removeImage }) => { return <div className="uploaded-files-container"> {files.map(file => ( <div className="uploaded-file-container" key={file.name}> <div className="thumbnail-container"> <div className="thumbnail" style={{backgroundImage: `url(${file.preview || '/_next/static/media/bg-searchTreatment.aa8648dea68731269b666fbe58cd505f.jpg'})`}}></div> </div> <div className="details-container"> <div className="progress-indicator" style={{width: `${file.progress || 0}%`}}></div> { file.progress && <div className="progress-number">{ file.progress }%</div> } <div className="file-name">{file.name}</div> <div className="file-size">{(file.size/1024).toFixed(0)}KB</div> </div> <div className="actions-container"> { file.progress && file.progress < 100 && <Spinner /> } { !uploading && <a onClick={() => removeImage(file)}><Image src="/assets/remove.svg" height="30" width="30" /></a> } </div> </div> ))} </div> }; // 父组件内调用 <Thumbs files={files} uploading={uploading} removeImage={removeImage} />
内容的提问来源于stack exchange,提问作者Multitut
相关产品推荐
相关产品推荐

