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;
核心改动说明
- 彻底移除了直接修改props中
country对象的逻辑,所有数据修改遵循不可变原则,不会污染外部传入的属性 - 给useEffect添加了完整的依赖项,切换选中国家时会自动重新初始化数据,避免闭包导致的旧数据问题
- 新增loading状态,加载过程中给用户明确提示,避免无响应的交互体验
- 城市请求改用
Promise.all批量处理,所有城市数据拉取完成后统一更新状态和本地存储,减少了多次不必要的重复渲染 - 新增
AbortController处理请求取消,组件卸载或切换选中国家时会中断未完成的请求,避免内存泄漏和竞态问题 - 所有状态更新完全基于React的状态快照,不再依赖外部可变的全局变量,彻底解决状态更新不及时的问题
新手需要注意的React核心规则
- 永远不要直接修改props或者state的值,所有变更都要通过set方法,并且使用不可变修改(返回新的对象/数组,而不是修改原有值)
- useEffect用到的所有外部变量都要加到依赖数组里,否则会出现闭包陷阱,拿到的永远是第一次渲染的旧值
- 组件内的异步请求一定要处理取消逻辑,避免组件卸载后还执行setState导致报错
内容的提问来源于stack exchange,提问作者Junaid
相关产品推荐
相关产品推荐

