React两个useEffect执行时序问题致渲染时ride.distance为undefined
问题根源
这个现象和useEffect的执行顺序没有直接关系,核心是代码违反了React状态更新的几个规则:
- React的setState是异步的,调用
setUpdatedRides(rides)后,当前函数作用域内的updatedRides变量还是旧值(初始为空数组),不会立刻变成刚传入的rides,你后续对updatedRides.map的调用实际是在空数组上执行的,根本没遍历到接口返回的ride数据。 - 你直接给ride对象挂载属性
ride.distance = xxx属于可变修改,React无法检测到这种对象内部属性的变化,不会触发重渲染,哪怕属性加上了页面也拿不到最新值。 - 第二个useEffect的依赖数组只写了
[rides],但逻辑里用到了user状态,存在闭包陷阱:当rides更新触发effect执行时,user可能还是初始空对象,拿不到station_code。 - map里传入async回调时,map不会等待异步操作执行完成,你也没有在所有distance计算完成后把新的列表重新set回状态,渲染逻辑自然读不到distance字段。
你在effect里打印ride能看到distance,是因为控制台打印的是对象的引用,等异步计算完成给对象加上distance属性后,你在控制台展开对象看到的就是修改后的值,但这个变化React完全感知不到。
React组件的正常执行流程
你猜测的“第一个useEffect执行完就触发渲染、第二个useEffect延后执行”确实是React的默认表现:
- 组件首次挂载时先执行return逻辑完成第一次渲染,此时三个状态都是初始值:rides、updatedRides为空数组,user为空对象。
- 首次渲染完成后执行第一个useEffect,发起接口请求,请求返回后每次调用setState都会触发一次组件重渲染。
- setRides执行后rides状态更新,组件重渲染,第二个useEffect因为依赖的rides变化,会在这次渲染完成后执行,但因为前面说的几个逻辑错误,没有把算好distance的列表正确更新到状态中,所以渲染时ride.distance始终是undefined。
不需要刻意阻塞渲染等第二个useEffect执行完,只要修正状态更新逻辑,数据准备好之后React自然会自动重渲染展示正确内容。
修复后代码
直接用useMemo派生计算带distance的列表即可,不需要额外维护updatedRides状态,也能避免状态不同步的问题:
import { useState, useEffect, useMemo } from "react"; import Navbar from "./Navbar"; // 纯计算逻辑抽到组件外,避免重复创建 const calculateDistance = (path, userStation) => { if (!path?.length || userStation == null) return 0; let min = Math.abs(userStation - path[0]); for (let i = 0; i < path.length; i++) { if (path[i] === userStation) return 0; const gap = Math.abs(path[i] - userStation); if (gap < min) min = gap; } return min; }; const Rides = () => { const [rides, setRides] = useState([]); const [user, setUser] = useState({}); useEffect(() => { const fetchData = async () => { try { // 两个接口无依赖,用Promise.all并行请求提升加载速度 const [ridesRes, userRes] = await Promise.all([ fetch("https://assessment.api.vweb.app/rides").then((res) => res.json()), fetch("https://assessment.api.vweb.app/user").then((res) => res.json()), ]); setRides(ridesRes); setUser(userRes); } catch (e) { console.log(e); } }; fetchData(); }, []); // 依赖rides和user,数据变化时自动计算带distance的列表 const updatedRides = useMemo(() => { if (!rides.length || !user.station_code) return []; // 不修改原对象,返回新的副本符合React不可变要求 return rides.map((ride) => ({ ...ride, distance: calculateDistance(ride.station_path, user.station_code), })); }, [rides, user]); return ( <div> <Navbar user={user} /> <div className="rides"> {updatedRides.map((ride) => ( <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;
关键修复点
- 移除了有逻辑问题的第二个useEffect和多余的updatedRides状态,用useMemo做派生计算,从根源避免状态不同步。
- 计算时不直接修改原ride对象,通过展开运算符生成新对象,React可以正确检测到变化触发渲染。
- 补全了依赖项,rides和user任意一个加载完成都会自动重新计算,不会出现拿不到用户站点编码的问题。
- 两个接口改为并行请求,缩短页面加载等待时间。
- 给列表项添加了唯一key,消除React渲染警告,同时加了初始值判断,避免数据未加载完成时报错。
内容的提问来源于stack exchange,提问作者Anuj kumar Jaimini
相关产品推荐
相关产品推荐

