React useEffect内forEach循环未执行及fetch异步问题排查
useEffect内部forEach循环未进入执行问题解答
问题根因
forEach未执行的本质是两个问题叠加:
- 闭包旧值问题:第二个fetch的then回调中读取的
updatedRides,是useEffect首次执行时闭包保存的初始值——也就是空数组[]。React的state更新是异步的,setUpdatedRides触发的重渲染不会修改当前已经在执行的useEffect回调里的变量值,空数组调用forEach自然不会进入循环体。 - 请求时序不可控:两个平级编写的fetch请求是并发发起的,没有先后等待关系,哪个请求先返回完全取决于网络速度。如果用户信息接口比行程接口先返回,此时行程数据还未获取到,
updatedRides必然是空值。
疑问答复
- 关于「是否可以在fetch的then回调中调用自定义函数」:完全可以。then回调本质是普通的JavaScript异步回调函数,只要自定义函数在当前作用域可访问,就可以正常调用,你代码中
calculateDistance的调用本身不存在语法问题。 - 关于「两个fetch的执行顺序以及如何实现顺序执行」:
你当前写法下两个fetch是并发执行的,调用fetch方法后请求会立刻发出,不会等待前一个请求返回再发起下一个。
要实现useEffect内逻辑按顺序执行,常用两种方案:- 方案1:Promise链式调用,把第二个fetch放到第一个fetch的then回调中,等前一个请求拿到结果后再发起下一个请求
- 方案2:使用async/await语法(可读性更高),在useEffect内定义异步函数,按顺序await两个请求的返回结果,拿到所有需要的数据后再做遍历计算
注意:不要在setState调用之后,立刻在当前回调中读取刚更新的state做计算,直接使用接口返回的原始数据计算即可,从根源上避免闭包拿到旧state值的问题。
修正后可运行代码
import { useEffect, useState } from "react"; import Navbar from "./Navbar"; import RideDetails from "./RideDetails"; const Rides = () => { const [rides, setRides] = useState([]); const [user, setUser] = useState({}); const [updatedRides, setUpdatedRides] = useState([]); useEffect(() => { const fetchData = async () => { try { // 先请求行程数据 const ridesRes = await fetch('https://assessment.api.vweb.app/rides', { method: 'GET', headers: { 'Content-Type': 'application/json' } }); if(!ridesRes.ok) throw new Error('Unable to fetch Rides data'); const ridesResult = await ridesRes.json(); // 行程数据返回后再请求用户数据 const userRes = await fetch('https://assessment.api.vweb.app/user', { method: 'GET', headers: { 'Content-Type': 'application/json' } }); if(!userRes.ok) throw new Error('Unable to fetch User'); const userResult = await userRes.json(); // 直接用接口返回的原始数据计算,不依赖闭包中的state const processedRides = ridesResult.map(ride => { return { ...ride, distance: calculateDistance(ride.station_path, userResult.station_code) } }); // 统一更新所有state setRides(ridesResult); setUser(userResult); setUpdatedRides(processedRides); } catch (e) { console.log(e); } } fetchData(); }, []) function calculateDistance (path, user_station) { let min = Math.abs(user_station - path[0]); for(let i = 0; i<path.length; i++){ if(path[i] === user_station){ return 0; } const currentDistance = Math.abs(path[i] - user_station); if(currentDistance < min){ min = currentDistance; } } return min; } return( <div> <Navbar user = {user}/> <div className="rides"> {updatedRides.map((ride) => { return ( <div className="rideDetail" key={ride.id}> <img src = {ride.map_url} alt="Ride_map" /> <div> <p>Ride Id : {ride.id}</p> <p>Origin Station : {ride.origin_station_code}</p> <p>Station Path : {ride.station_path}</p> <p>Date : {ride.date}</p> <p>Distance : {ride.distance}</p> </div> </div> ) })} </div> </div> ) } export default Rides;
修正点说明
- 用async/await保证请求按顺序执行,等两个接口数据都返回后再做计算
- 距离计算直接使用接口返回的原始数据,完全规避闭包读取旧state的问题
- 遍历行程数据时用map生成新数组,遵循React不可变数据更新原则,不直接修改原接口返回的对象属性
- 给列表渲染项增加
key={ride.id},消除React列表渲染的警告
内容的提问来源于stack exchange,提问作者Anuj kumar Jaimini
相关产品推荐
相关产品推荐

