使用useEffect Hook拉取数据时URL参数变更,React组件重渲染但获取旧数据
问题背景
- 业务场景:存在1个需为≈25个不同条目/页面复用相同结构的组件,实现逻辑是将动态URL参数传入API请求完成数据拉取。
- 组件内调用自定义Hook的代码如下:
const [{ items, isLoading, isError }] = useDataApi( `http://localhost:5000/api/teams/topspending/${params.team}`, [], params.team );
- API请求逻辑被封装为独立的自定义Hook,用于简化重复逻辑,Hook初始实现代码如下:
const useDataApi = (initialUrl, initialData, effect) => { console.log("start/top Data API"); const [items, setItems] = useState(initialData); const [url, setUrl] = useState(initialUrl); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); useEffect(() => { setUrl(initialUrl); const abortCtrl = new AbortController(); const opts = { signal: abortCtrl.signal }; const fetchData = async () => { setIsError(false); setIsLoading(true); console.log("data loading"); try { console.log(url, "this should be the new Url"); const result = await axios(url, opts); setItems(result.data.data); } catch (error) { setIsError(true); } console.log("data loaded..."); setIsLoading(false); }; fetchData(); return () => abortCtrl.abort(); }, [effect]); return [{ items, isLoading, isError }]; }; export default useDataApi;
需求与异常表现
- 核心需求:点击导航栏的导航链接,当请求URL从
http://localhost:5000/api/teams/topspending/Team1切换为http://localhost:5000/api/teams/topspending/Team2时,被复用的同一组件能基于更新后的URL拉取最新数据,完成正确重渲染。 - 异常表现:尝试过多种调整方案,虽然能让组件在每次URL变更后触发更新,但拉取到的数据始终为旧数据。目前该单组件的路由配置基于React-Router实现。
问题根因
- 状态更新时序错误:
useEffect回调中先执行setUrl(initialUrl),但React的状态更新是异步批处理的,同步执行后续代码时读取到的url变量仍然是上一次渲染阶段的旧值,导致请求始终命中旧URL。 - 依赖数组配置错误:
useEffect的依赖仅传入了effect(即动态路由参数params.team),没有把实际请求使用的URL纳入依赖追踪,状态更新和请求触发逻辑完全脱节。 - 冗余的url状态没有存在的必要,只会加剧状态同步的混乱。
修正实现
直接调整Hook的实现逻辑,去掉冗余的url状态,把请求逻辑的依赖和实际传入的参数对齐:
import { useState, useEffect } from 'react'; import axios from 'axios'; const useDataApi = (url, initialData, effect) => { const [items, setItems] = useState(initialData); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); useEffect(() => { const abortCtrl = new AbortController(); const opts = { signal: abortCtrl.signal }; const fetchData = async () => { setIsError(false); setIsLoading(true); try { const result = await axios(url, opts); setItems(result.data.data); } catch (error) { // 跳过主动中断的请求触发的错误状态 if (!abortCtrl.signal.aborted) { setIsError(true); } } setIsLoading(false); }; fetchData(); return () => abortCtrl.abort(); // 加入请求URL和触发参数作为依赖 }, [url, effect]); return [{ items, isLoading, isError }]; }; export default useDataApi;
实现说明
- 去掉了手动调用
setUrl的冗余逻辑:传入的url本身就是跟着组件渲染同步更新的最新值,只要把它纳入useEffect的依赖数组,就能在路由参数变更、URL重新拼接完成后自动触发请求,避免异步状态更新导致的读旧值问题。 - 增加中断请求的状态判断,避免路由快速切换时,旧的已被中断的请求把错误状态误置为true。
内容的提问来源于stack exchange,提问作者DOZBORNE
相关产品推荐
相关产品推荐

