React异步setState未生效:合并API数据到info状态失败
问题背景
我有一个app.jsx文件,会请求API_A与API_B两个不同接口,将获取到的data_A和data_B合并到info状态中用于UI渲染。控制台输出显示监听info的useEffect未触发,data_B未成功合并到info状态中,看起来setState已执行但未生效,React版本为17.0.48。
代码示例
const [info, setInfo] = useState(); function go(){ API_A().then(({ data }) => { setInfo((prev) => { const t = { ...prev, ...data, }; console.log('===== set API_A =====', t); // 深克隆,实际场景中返回的是对象 return JSON.parse(JSON.stringify(t)); }); }); API_B().then(({ data: res = {} }) => { setInfo(prev => { const t = { ...prev, ...res, }; console.log('===== set API_B =====', t); // 深克隆,实际场景中返回的是对象 return JSON.parse(JSON.stringify(t)); }); }); } useEffect(() => { go(); }, []) useEffect(() => { console.log('=====useEffect info', info); }, [info])
原因解析
异步回调的状态竞态问题
两个API请求并行发起,它们的then回调异步执行。在React 17中,异步回调内的setState默认会被批量处理,但两个回调获取到的prev状态都是初始值undefined(useState未设置初始值)。当两个回调几乎同时执行时,后执行的setState会基于初始状态合并数据,直接覆盖前一个setState的结果,导致其中一个接口的数据丢失。状态浅比较导致的渲染跳过
React会对setState前后的状态进行浅比较,如果两次更新后的状态对象在浅比较下相等(比如两次合并后的对象结构/内容完全一致),React会跳过重渲染,因此监听info的useEffect不会触发。另外,初始状态为undefined,合并后的对象如果和某一次单独更新的对象一致,也会导致后续更新不触发渲染。
解决方案
方案1:合并API请求,一次性更新状态(推荐)
通过Promise.all等待两个接口请求完成后,一次性合并数据并更新状态,彻底避免竞态问题:
// 初始化为空对象,避免合并时出现undefined的问题 const [info, setInfo] = useState({}); function go(){ Promise.all([API_A(), API_B()]) .then(([resA, resB]) => { const mergedData = { ...resA.data, ...(resB.data || {}) }; // 保持原有深克隆逻辑 setInfo(JSON.parse(JSON.stringify(mergedData))); }) .catch(err => { // 处理请求错误 console.error('请求失败:', err); }); }
方案2:确保状态更新基于最新值(适用于必须单独请求的场景)
如果需要分别处理两个接口的返回结果,可以在更新时确保每次都基于最新的状态,但仍需注意竞态:
const [info, setInfo] = useState({}); function go(){ API_A().then(({ data }) => { setInfo(prev => { const updated = { ...prev, ...data }; return JSON.parse(JSON.stringify(updated)); }); }); API_B().then(({ data: res = {} }) => { setInfo(prev => { const updated = { ...prev, ...res }; return JSON.parse(JSON.stringify(updated)); }); }); }
注:这种方式仍可能因请求响应顺序导致状态覆盖,但比原代码更可靠,因为每次更新都基于当前最新的prev状态。如果接口响应差异较大,建议还是使用方案1。
额外优化
- 初始状态设置为空对象
useState({}),避免合并...prev时因为undefined导致的异常。 - 若不需要深克隆,可直接返回合并后的对象,减少不必要的性能开销。
内容的提问来源于stack exchange,提问作者chenglili

