如何在Promise循环中更新React状态?文件上传场景实践
问题分析与解决方案
问题根源
- 闭包捕获旧状态:
uploadItems执行时,items是初始空数组,forEach里所有Promise回调都捕获了这个旧值。每次调用setItems(items.concat(itm))都是基于空数组添加单个元素,最终UI只会保留最后完成的上传项。 - 并行执行违反WebDav限制:原代码用
forEach同时触发所有上传请求,既不符合WebDav逐个上传的要求,也加剧了状态覆盖问题。
修复方案
1. 解决状态更新的闭包问题
使用React函数式状态更新,它会接收当前最新的状态值,彻底避免闭包捕获旧状态的问题:
setItems(prevItems => prevItems.concat(itm));
2. 实现串行上传(符合WebDav要求)
如果需要严格逐个上传(上一个完成再发起下一个),将forEach替换为for...of循环,配合await实现串行执行:
修改后的uploadItems函数:
async function uploadItems() { for (const item of data) { const uploadedItem = await doUpload(item.id); setItems(prev => prev.concat(uploadedItem)); // 如需额外延迟,可在此添加 await delay(); } }
完整修复代码
const data = [ { id: 1, name: "Marry" }, { id: 2, name: "had" }, { id: 3, name: "a" }, { id: 4, name: "little" }, { id: 5, name: "lamb" }, ]; export default function App() { const [items, setItems] = useState([]); return ( <div className="App"> <button onClick={uploadItems}>Upload items</button> <ul> {items.map((itm) => ( <li key={itm.id}>{itm.name}</li> {/* 添加key符合React列表渲染规范 */} ))} </ul> </div> ); async function uploadItems() { // 串行逐个发起上传请求 for (const item of data) { const uploadedItem = await doUpload(item.id); // 基于最新状态更新列表 setItems(prevItems => prevItems.concat(uploadedItem)); } } } function doUpload(id) { const timeout = Math.round(Math.random() * 1000); return new Promise((resolve) => setTimeout(() => resolve(data.find((itm) => itm.id === id)), timeout) ); } function delay() { return new Promise((resolve) => setTimeout(resolve, 0)); }
额外说明
- 函数式状态更新是React处理异步状态更新的标准方案,能有效规避闭包带来的旧状态问题。
- 串行上传既满足WebDav的限制,也能让UI按上传顺序逐步更新,不会出现状态覆盖。
- 给列表项添加
key属性,符合React列表渲染规范,避免不必要的重渲染和控制台警告。
内容的提问来源于stack exchange,提问作者Sergey Poskachey
相关产品推荐
相关产品推荐

