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

React使用Hooks拉取API数据结合localStorage优化三级联动请求问题

重构后符合React规范的代码
import React, { useState, useEffect } from "react";
import httpService from "../services/httpService";

const ExpandComponent = ({ countries, data: { country: selectedCountry } }) => {
  // 状态:当前国家的州/城市数据、加载状态
  const [countryStates, setCountryStates] = useState([]);
  const [loading, setLoading] = useState(false);

  // 当选中国家变化时,初始化数据/发起请求
  useEffect(() => {
    // 取消上一次未完成的请求,避免竞态
    const abortController = new AbortController();
    const signal = abortController.signal;

    // 找到当前选中的国家数据
    const currentCountry = countries.find(m => m.country === selectedCountry);
    
    // 如果已有缓存的州数据,直接用,不需要请求
    if (currentCountry?.states) {
      setCountryStates(currentCountry.states);
      return;
    }

    // 没有缓存,发起请求
    setLoading(true);
    // 拉取州数据
    const fetchData = async () => {
      try {
        const stateRes = await httpService.get(
          `http://localhost/states?country=${selectedCountry}`,
          { signal }
        );
        const states = stateRes.data.data || [];
        // 先更新州数据到本地存储和状态
        updateStorage(states);
        setCountryStates(states);

        // 并行拉取所有州对应的城市数据
        const cityRequests = states.map(async state => {
          try {
            const cityRes = await httpService.get(
              `http://localhost/cities?state=${state.state}&country=${selectedCountry}`,
              { signal }
            );
            return {
              ...state,
              cities: cityRes.data.data || []
            };
          } catch (err) {
            if (err.response?.status === 400) {
              return { ...state, cities: [] };
            }
            throw err;
          }
        });

        // 等待所有城市请求完成
        const statesWithCities = await Promise.all(cityRequests);
        // 统一更新状态和本地存储
        updateStorage(statesWithCities);
        setCountryStates(statesWithCities);
      } catch (err) {
        if (err.name !== 'AbortError' && err.response?.status === 400) {
          updateStorage([]);
          setCountryStates([]);
        }
      } finally {
        if (!signal.aborted) {
          setLoading(false);
        }
      }
    };

    fetchData();

    // 组件卸载/切换国家时取消未完成的请求
    return () => abortController.abort();
  }, [selectedCountry, countries]);

  // 更新localStorage逻辑,完全基于不可变修改,不改动原props
  const updateStorage = (states) => {
    const updatedCountries = countries.map(p => 
      p.country === selectedCountry 
        ? { ...p, states } 
        : p
    );
    localStorage.setItem("countriesData", JSON.stringify(updatedCountries));
  };

  // 加载中提示
  if (loading) {
    return <div><span className="text-info">正在加载数据...</span></div>;
  }

  // 无数据提示
  if (!countryStates?.length) {
    return <div><span className="text-danger">No states available.</span></div>;
  }

  return (
    <div>
      <table className="table table-secondary table-hover table-sm">
        <thead>
          <tr>
            <th>State</th>
            <th>Cities</th>
          </tr>
        </thead>
        <tbody>
          {countryStates.map((m, index) => (
            <tr key={index}>
              <td>{m.state}</td>
              <td>
                {m?.cities?.length ? (
                  m.cities.map(cityItem => cityItem.city).join(", ")
                ) : (
                  <span className="text-danger">No cities available.</span>
                )}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default ExpandComponent;

核心改动说明

  1. 彻底移除了直接修改props中country对象的逻辑,所有数据修改遵循不可变原则,不会污染外部传入的属性
  2. 给useEffect添加了完整的依赖项,切换选中国家时会自动重新初始化数据,避免闭包导致的旧数据问题
  3. 新增loading状态,加载过程中给用户明确提示,避免无响应的交互体验
  4. 城市请求改用Promise.all批量处理,所有城市数据拉取完成后统一更新状态和本地存储,减少了多次不必要的重复渲染
  5. 新增AbortController处理请求取消,组件卸载或切换选中国家时会中断未完成的请求,避免内存泄漏和竞态问题
  6. 所有状态更新完全基于React的状态快照,不再依赖外部可变的全局变量,彻底解决状态更新不及时的问题

新手需要注意的React核心规则

  • 永远不要直接修改props或者state的值,所有变更都要通过set方法,并且使用不可变修改(返回新的对象/数组,而不是修改原有值)
  • useEffect用到的所有外部变量都要加到依赖数组里,否则会出现闭包陷阱,拿到的永远是第一次渲染的旧值
  • 组件内的异步请求一定要处理取消逻辑,避免组件卸载后还执行setState导致报错

内容的提问来源于stack exchange,提问作者Junaid

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.30 16:36:01