React异步请求致cityWeathers数组长度异常问题排查
问题背景
业务逻辑为先调用monday.com的API获取数据,再根据返回结果中的城市信息调用天气API查询各城市天气,最终为每个匹配到的城市渲染天气卡片。预期cityWeathers数组应包含9个元素,但实际运行时每次得到的数组长度不固定,可能为3、5、7不等,始终无法获取完整数据。
原始问题代码
constructor(props) { super(props); this.state = { setData: {}, context:{}, settings: {}, myData: { boards: [] }, cityWeathers:[] }; } componentDidMount() { const getWeather = async () => monday.api('query { boards( ids : xxxxxxxxxx ) { items { id : name column_values { text }}}}') .then((res) => { this.setState({myData:res.data}); this.state.myData.boards.map((board) => { board.items.map((item) =>{ fetch(`https://api.weatherapi.com/v1/current.json?key=xxxxxxxxxxxxxxxxxxxxxx=${item.column_values[3].text}&aqi=no`) .then((res) => res.json()) .then((json) => { let cityWeather ={ name: json.location.name, temp_c:json.current.temp_c, temp_f:json.current.temp_f, condition: json.current.condition.text, localTime: json.location.localtime, icon: json.current.condition.icon }; let cityWeathers = [...this.state.cityWeathers, cityWeather]; this.setState({cityWeathers}); }) })} ) } ) getWeather() } render(){ return( <div className="App"> <div className="container"> <div className="row"> <div className="col-md-4"> {console.log(this.state.cityWeathers.length)} { this.state.cityWeathers.map((city) => <WeatherCard key={city.name} className="cards" name={city.name} temp_c={city.temp_c} temp_f={city.temp_f} icon={city.icon} condition={city.condition} localtime={city.localTime} />) } </div> </div> </div> </div> ) } }
故障原因
- 核心诱因是并行异步请求的竞态问题+错误的状态更新逻辑:循环中发起的9个天气接口请求是并行的,返回时间完全随机。每次请求返回后更新状态时,都是直接读取当前闭包里的
this.state.cityWeathers旧值做追加,多个请求几乎同时返回时,后返回的请求读到的可能还是未被前序请求更新过的空数组,最终会覆盖之前已经追加的内容,导致数组长度随机。 setState是异步行为,调用this.setState({myData:res.data})后立刻读取this.state.myData,大概率拿到的还是初始默认值,不是接口返回的最新数据,会埋下数据读取异常的隐患。- 所有并行的天气请求没有做统一的收敛等待,也没有加错误捕获,任意一个请求失败都会导致对应城市的数据缺失,且没有报错日志无法感知。
- 遍历数组执行副作用操作时错误使用了
map方法,map的设计用途是映射生成新数组,纯遍历执行逻辑应该用forEach,语义更准确。
修复方案
- 拿到monday接口的返回结果后,直接使用接口响应值做遍历,不要依赖异步更新后的state
- 把所有城市的天气请求Promise收集起来,用
Promise.all等待所有请求全部完成后,一次性更新cityWeathers状态,从根源上避免竞态覆盖问题 - 增加请求错误捕获,避免单个请求失败导致整个逻辑崩溃
- 遍历执行副作用时替换为
forEach
修复后的参考代码:
constructor(props) { super(props); this.state = { setData: {}, context:{}, settings: {}, myData: { boards: [] }, cityWeathers:[] }; } async componentDidMount() { try { // 先请求monday数据,直接使用响应结果,不依赖异步更新的state const mondayRes = await monday.api('query { boards( ids : xxxxxxxxxx ) { items { id : name column_values { text }}}}'); this.setState({myData: mondayRes.data}); // 收集所有天气请求Promise const weatherRequestList = []; mondayRes.data.boards.forEach(board => { board.items.forEach(item => { const weatherReq = fetch(`https://api.weatherapi.com/v1/current.json?key=xxxxxxxxxxxxxxxxxxxxxx=${item.column_values[3].text}&aqi=no`) .then(res => res.json()) .then(json => ({ name: json.location.name, temp_c: json.current.temp_c, temp_f: json.current.temp_f, condition: json.current.condition.text, localTime: json.location.localtime, icon: json.current.condition.icon })) .catch(err => { console.error('获取城市天气失败:', item.column_values[3].text, err); return null; }); weatherRequestList.push(weatherReq); }) }); // 等待所有天气请求完成,过滤掉请求失败的空值,一次性更新state const weatherList = await Promise.all(weatherRequestList); const validWeatherList = weatherList.filter(Boolean); this.setState({ cityWeathers: validWeatherList }); } catch (err) { console.error('请求monday数据失败:', err); } } render(){ return( <div className="App"> <div className="container"> <div className="row"> <div className="col-md-4"> {this.state.cityWeathers.map((city) => <WeatherCard key={city.name} className="cards" name={city.name} temp_c={city.temp_c} temp_f={city.temp_f} icon={city.icon} condition={city.condition} localtime={city.localTime} /> )} </div> </div> </div> </div> ) }
内容的提问来源于stack exchange,提问作者Kintaro
相关产品推荐
相关产品推荐

