React路由切换时如何保存组件状态避免重复发起API请求
问题核心原因
路由切换时ProductList组件被完整卸载,组件内部state会被全部回收,所以每次返回都会重新执行初始化逻辑触发请求。你之前用的localStorage方案偶发失效,基本是这几个问题导致的:
- 存储时机不对:把存数据的逻辑放在卸载阶段,如果跳转时接口请求还没完成,存进去的就是初始空数组
- 缺少校验:没有判断缓存是否有效、是否过期,容易拿到格式错误或者过旧的数据
- 没有拦截请求:读完缓存之后还是执行了后续请求逻辑,等于没做缓存
可落地实现方案
按改造成本从低到高排序,选一个适合你项目的就行:
方案1:修复localStorage缓存逻辑(零额外依赖)
不用改现有项目结构,只要补全缓存读写的校验逻辑,不要等卸载时再存数据,每次拿到新数据立刻同步缓存即可,参考修改后的代码:
import React, { useState, useEffect} from "react"; import ProductItem from "../Components/ProductItem"; import axios from "axios"; // 缓存配置 const CACHE_KEY = "hotel_list_cache"; const CACHE_TTL = 10 * 60 * 1000; // 缓存有效期10分钟,可自行调整 const ProductList = () => { // 初始化时优先读有效缓存 const [hotelList, setHotelList] = useState(() => { const cacheStr = localStorage.getItem(CACHE_KEY); if (cacheStr) { try { const cache = JSON.parse(cacheStr); // 缓存没过期就直接用 if (cache.expireTime > Date.now()) { return cache.data; } } catch (err) { // 缓存格式错误直接清掉 localStorage.removeItem(CACHE_KEY); } } return []; }); // 统一更新列表和缓存,不用等卸载再存 const updateHotelList = (list) => { setHotelList(list); localStorage.setItem(CACHE_KEY, JSON.stringify({ data: list, expireTime: Date.now() + CACHE_TTL })); }; // Get user location by IP const getCurrentLocation = () => { return fetch("https://ipinfo.io/json?token=MyToken").then( (response) => response.json() ); }; // Get list of hotels in specific location const getHotelsInLocation = (destInfo) => { console.log('destInfo is: ', destInfo) const options = { method: "GET", url: "https://booking-com.p.rapidapi.com/v1/hotels/search", params: { checkout_date: "2022-10-01", units: "metric", dest_id: destInfo.destId, dest_type: destInfo.destType, locale: "en-gb", adults_number: 2, order_by: "popularity", filter_by_currency: "USD", checkin_date: "2022-09-30", room_number: 1, }, headers: { "X-RapidAPI-Host": "booking-com.p.rapidapi.com", "X-RapidAPI-Key": "MyApiKey", }, }; axios .request(options) .then(function (response) { console.log(response.data.result); updateHotelList(response.data.result); }) .catch(function (error) { console.error(error); }); }; useEffect(() => { // 已有有效缓存直接跳过所有请求 if (hotelList.length > 0) return; getCurrentLocation().then((currentLocation) => { console.log("Current city ", currentLocation.city); const options = { method: "GET", url: "https://booking-com.p.rapidapi.com/v1/hotels/locations", params: { locale: "en-gb", name: currentLocation.city }, headers: { "X-RapidAPI-Host": "booking-com.p.rapidapi.com", "X-RapidAPI-Key": "MyApiKey", }, }; axios .request(options) .then(function (response) { console.log(response.data); let destId = response.data[0].dest_id; let destType = response.data[0].dest_type; const destInfo = { destId, destType }; getHotelsInLocation(destInfo); }) .catch(function (error) { console.error(error); }); }); }, [hotelList.length]); return ( <> {hotelList.map((hotel) => ( <ProductItem key={hotel.hotel_id} hotel={hotel} /> ))} </> ); }; export default ProductList;
这个方案适合小型项目,缺点是localStorage在隐私模式下可能被禁用,刷新页面缓存虽然还在,但如果用户清了浏览器存储就会重新请求。
方案2:状态上提(无兼容问题)
如果你的路由外层有不会随路由切换卸载的布局组件,直接把hotelList相关的state和请求逻辑挪到这个父组件上,通过props或者Context传给ProductList即可。只要父组件不销毁,状态就会一直保留,不需要做本地存储,也没有序列化、缓存失效的坑,适合路由结构简单的项目。
方案3:使用专业请求缓存库(生产环境推荐)
如果项目里有大量类似的列表缓存需求,直接用SWR、React Query这类成熟的请求库,自带内存缓存机制,只要配置好缓存有效期,同一个请求在有效期内不会重复发起,组件卸载再重新挂载时会直接从内存里取之前的返回数据,不需要自己写任何缓存读写逻辑,还能自动处理请求竞态、重试、后台刷新等问题,稳定性比自己手写缓存高很多。
内容的提问来源于stack exchange,提问作者casper
相关产品推荐
相关产品推荐

