React useMemo 无法获取更新后数组对象最新状态的原因及解决方案

本文深入剖析 React 中useMemo未能响应数组对象状态更新的根本原因——异步操作未正确等待 Promise 完成,导致setAsyncProcesses过早触发、依赖项引用未真正变更,并提供基于Promise.all的可靠解决方案。
不少开发者都遇到过这个坑:在 React 函数组件中使用 useMemo 时,发现它始终无法响应状态更新。反复调试后才发现,问题并非出在 React 本身,而是异步操作尚未完成就提前更新了状态。
具体问题出在哪里?请看下面这段代码:
asyncProcesses.forEach(async (process, i) => {
const newProcess = await checkUploadCsvProcess(process, options);
newAsyncProcesses.splice(i, 1, newProcess);
});
setAsyncProcesses(newAsyncProcesses);
从代码结构上看,逻辑似乎没有问题:遍历数组、逐一异步处理每一项、更新数组、最后设置状态。但问题在于——forEach 是同步方法,不会等待内部 async 回调执行完毕。所有 checkUploadCsvProcess 调用其实是并发启动的,而 splice 操作依赖的是闭包中的旧数组引用。当 setAsyncProcesses 被调用时,绝大多数异步操作尚未完成。最终 asyncProcesses 里依然保留着原始的 Promise 对象,引用地址没有改变,useMemo 自然无法感知任何变化。
因此,问题的核心在于:依赖项看似被更新了,但实际引用地址并未变化——因为 setState 触发得太早。
✅ 正确做法:用 map + Promise.all 替代 forEach + async
核心思路是:将所有异步更新任务收集为一个 Promise 数组,等全部解析完成后,再一次性更新状态:
// 正确实践:所有异步完成后再 setState
const updatePromises = asyncProcesses.map(async (process, i) => {
try {
const newProcess = await checkUploadCsvProcess(
process as AsyncProcessCsvUpload,
{
actionName: dictionary.review,
actionOnClick: () => {
na vigate(routes.linkedAccounting);
setIsOpenInDialog(AsyncProcessType.CsvUpload, true);
},
}
);
return newProcess ?? process; // 防止返回 undefined 导致数组长度变化
} catch (error) {
console.error('Failed to update process:', error);
return process;
}
});
// 确保所有 Promise 解析完毕后再更新
Promise.all(updatePromises).then((updatedProcesses) => {
setAsyncProcesses(updatedProcesses);
});
⚠️ 还有几个关键细节需要注意:
- 千万不要直接修改 state 数组中的对象。例如写
process.snackbarType = 'success'这类代码,直接违反了 React 的不可变更新原则。即使对象内部属性被修改,useMemo监测的依赖项引用没有变化,它永远不会重新计算。checkUploadCsvProcess必须返回新对象(或深拷贝后的对象),而不是就地修改。推荐采用纯函数式风格:
// 返回新对象,保持不可变性 return { ...process, isActive: status === IntegrationStatus.UPLOAD, snackbarType: status === IntegrationStatus.ERROR ? 'error' : status === IntegrationStatus.SUCCESS ? 'success' : 'uploading', progress: status === IntegrationStatus.UPLOAD ? Math.round((logs?.info?.progress || 0) / (logs?.info?.transactionCount || 1) * 100) : undefined, snackbarAction: status !== IntegrationStatus.UPLOAD ? reviewAction : undefined, data: { ...process.data, errors: status === IntegrationStatus.ERROR ? logs?.error || {} : undefined, }, };useMemo的依赖项[asyncProcesses, displayAsSnackbarList, dictionary, removeAsyncProcess]本身是合理的,但前提是asyncProcesses必须是全新引用——即通过setAsyncProcesses([...])或setState(prev => [...prev])来触发重新渲染。
总结
回到最初的问题:useMemo 不触发更新,并不是 Hook 本身的缺陷,而是状态更新的时机选择错误,导致依赖项“看似变化,实际引用未变”。修复方法其实很简单,只需两点:
- 用
Promise.all(map(...))替代forEach(async)——确保所有异步操作完成后再调用setState; - 始终坚持不可变更新——永远返回新的数组和新的对象,杜绝就地修改。
只要 asyncProcesses 的引用发生了真正的变化,useMemo 就会重新计算,snackbarType 等字段也会自动更新。别让时序陷阱拖慢你的开发效率。
