You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的默认表现:

  1. 组件首次挂载时先执行return逻辑完成第一次渲染,此时三个状态都是初始值:rides、updatedRides为空数组,user为空对象。
  2. 首次渲染完成后执行第一个useEffect,发起接口请求,请求返回后每次调用setState都会触发一次组件重渲染。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 15:30:49