React应用RTK Query触发429错误:请求重复与触发条件问题
RTK Query 请求过多导致429错误及缓存失效问题
问题描述
在React应用中使用RTK Query从RapidAPI获取天气数据,数据时而能渲染时而失败。失败时控制台报429(请求过多)错误。仅在一处使用RTK Query,但提交表单等操作引发组件重渲染时,RTK钩子会重复发起请求,怀疑是问题根源。
疑问
- 单次更新中多次重渲染时,首次重渲染触发RTK请求,后续无参数变化的重渲染应该用缓存数据,为何还会出现429错误?
- 能否像useEffect一样,仅在依赖项变化时触发RTK钩子执行?
相关代码
App.js
import {createContext} from 'react' import './App.css'; import {Provider} from 'react-redux'; import {BrowserRouter as Router, Routes, Route} from 'react-router-dom' import SearchComponent from './components/SearchComponent' import NavBarComponent from './components/NavBarComponent' import {useGetSearchLocationQuery, useGetCurrentWeatherApiQuery} from './redux/services/weatherApi' import {useGetCityQuery} from './redux/services/countryApi' import {setCity} from './redux/slices/locationSlice' import {useSelector} from 'react-redux' import Today from './pages/Today' import Home from './pages/Home' import Week from './pages/Week' export const Context = createContext(); const App=()=> { const {city} = useSelector(state => state.location ) // 获取用户地理位置 const {data: fetchedCity, isLoading, isError, isFetching} = useGetCityQuery() // 获取城市ID const {data: locationData, isLoading: isLoadingId, isError: isErrorId} = useGetSearchLocationQuery( city || fetchedCity?.name ) if(isLoading || isLoadingId) return <p>data loading from App</p> if(!locationData) return <p>No location data from App</p> return ( <Context.Provider value={{fetchedCity,locationData}} > <div className="App"> <NavBarComponent /> <div className='pageContainer'> <SearchComponent/> <div className='mainSectionContainer'> <Routes> <Route path='/' element={<Home />} /> <Route path= '/today/:city' element={<Today /> }/> <Route path= '/week/:city' element={<Week /> }/> </Routes> </div> </div> </div> </Context.Provider> ); } export default App;
SearchComponent.js
import React, {useState, useContext, useEffect} from 'react' import api from '../assets/api' import '../style/searchBarStyle.css' import {GoSearch} from 'react-icons/go' import {TiDelete} from 'react-icons/ti' import { IconContext } from "react-icons"; import {weatherIconsArray} from '../assets/api' import {useDispatch, useSelector} from 'react-redux' import {setCity, setClickedCardIndex} from '../redux/slices/locationSlice' import {useGetSearchLocationQuery, useGetCurrentWeatherApiQuery} from './redux/services/weatherApi' import {Context} from '../App'; import {useNavigate} from 'react-router-dom' const SearchComponent = () =>{ const navigate = useNavigate(); const dispatch = useDispatch() const [searchInput, setSearchInput] = useState('') const {city} = useSelector(state =>state.location) const {locationData, fetchedCity} = useContext(Context) // 根据城市ID获取当前天气 const {data: currentWeatherData, isLoading: isCurrentLoading, isError: isErrorCurrent} = useGetCurrentWeatherApiQuery(locationData?.locations[0]?.id) const {image, description} = weatherIconsArray?.find(item => currentWeatherData?.current?.symbol === item.symbol) || {} useEffect(()=>{ if(city === fetchedCity?.name) setSearchInput('') },[city]) const handleSubmit=(e) =>{ e.preventDefault(); if(!searchInput){ alert("Please set a location in the search field") return } dispatch(setCity(searchInput)) navigate(`/today/${searchInput}`) } if(isCurrentLoading) return <p>is loading from searchbar</p> if(!currentWeatherData) return <p>No current forecast available</p> return( <div className='sidebarContainer'> <form onSubmit={handleSubmit}> <div className="searchContainer"> <GoSearch className='icon'/> <input value={searchInput} onChange={(e) =>{setSearchInput(e.target.value)}} placeholder='search'/> <IconContext.Provider value={{color: 'black'}}> <TiDelete onClick={()=>{setSearchInput('')}} className='deleteIcon'/> </IconContext.Provider> </div> </form> <div className='weatherImg'> <img src={image}/> </div> <div className='temperature'> <p>{currentWeatherData?.current?.temperature}°C</p> <p>{currentWeatherData?.time}</p> </div> <hr className='separator'/> <div className='weatherState'> <p>{description}</p> <p>{ `Wind speed: ${currentWeatherData?.current?.windSpeed} m/s`}</p> </div> <div className='countryAndCity'> <p> {`${locationData?.locations[0]?.name}, ${locationData?.locations[0]?.country}`}</p> </div> </div> ) } export default SearchComponent
Today.js
import React , {useEffect, useContext} from 'react' import { Swiper, SwiperSlide } from 'swiper/react' import {Pagination, Navigation} from 'swiper' import 'swiper/css'; import 'swiper/css/navigation'; import 'swiper/css/pagination'; import {Link, useParams} from 'react-router-dom' import TwoHoursCard from '../components/TwoHoursCard' import UvDetailsCard from '../components/UvDetailsCard' import WindDetailsCard from '../components/WindDetailsCard' import SunDetailsCard from '../components/SunDetailsCard' import '../style/todayStyle.css' import {useSelector, useDispatch} from 'react-redux' import {useGetNowCastQuery, useGetWeekCastQuery} from './redux/services/weatherApi' import {weatherIconsArray} from '../assets/api' import {setCity, setClickedCardIndex, setPreviousUvIndex} from '../redux/slices/locationSlice' import {Context} from '../App' const Today = () =>{ const {clickedCardIndex,previousUvIndex} = useSelector(state => state.location) const dispatch = useDispatch() const {city} = useParams(); useEffect(()=>{ dispatch(setCity(city)) }, [city]) useEffect(()=>{ dispatch(setClickedCardIndex(-1)) dispatch(setPreviousUvIndex(NaN)) }, [clickedCardIndex]) const {locationData} = useContext(Context) // 获取未来2小时每15分钟的预报 const {data, isFetching, isLoading: isLoadingNow, isError: isErrorNow} = useGetNowCastQuery(locationData?.locations[0]?.id) // 获取一周预报 const {data: weekData, isLoading: isLoadingWeek, isError: isErrorWeek} = useGetWeekCastQuery(locationData?.locations[0]?.id) if(isLoadingNow) return <p>is loading two hours forecast</p> if(isLoadingWeek) return <p>is loading week</p> return( <> <p className="headers">Upcoming 2 hours:</p> { data ? ( <Swiper modules={[Navigation, Pagination]} navigation pagination={{clickable: true}} breakpoints={{ 0: {slidesPerView: 1}, 480:{ slidesPerView: 2, spaceBetween: 16, }, 768:{ slidesPerView: 4, spaceBetween: 16 }, 1024:{ slidesPerView: 6, spaceBetween: 16 }, 1150:{ slidesPerView: 8, spaceBetween: 16 } }} > { data && data?.forecast?.map((element, index) => ( <SwiperSlide> <TwoHoursCard key={index} description={ element.symbolPhrase || weatherIconsArray.find((obj) => obj.symbol === element?.symbol)?.description} image = {weatherIconsArray.find((obj) =>obj.symbol === element?.symbol).image} temperature={element.temperature} /> </SwiperSlide> )) } </Swiper> ) : (<p>No estimation available for the next coming 2 hours</p>) } <div> <p className="headers">Today's highlight:</p> <div className = 'todayDetails'> <UvDetailsCard weekData={weekData} index={1} clickedCardIndex={clickedCardIndex}/> <WindDetailsCard weekData={weekData} index={2} clickedCardIndex={clickedCardIndex}/> <SunDetailsCard weekData={weekData} index={3} clickedCardIndex={clickedCardIndex}/> <WindDetailsCard weekData={weekData} index={4} clickedCardIndex={clickedCardIndex}/> <WindDetailsCard weekData={weekData} index={5} clickedCardIndex={clickedCardIndex}/> <SunDetailsCard weekData={weekData} index={6} clickedCardIndex={clickedCardIndex}/> </div> </div> </> ) } export default Today
countryApi.js
import {createApi, fetchBaseQuery} from '@reduxjs/toolkit/query/react' const cryptoNewsHeader = { 'x-bingapis-sdk': 'true', 'x-rapidapi-host': 'spott.p.rapidapi.com', 'x-rapidapi-key': '6e4bb62ec0msh084a4eb0f5c4a3bp149a1ejsn039034dc54d7', }; const createRequest = (url) => ({ url, headers: cryptoNewsHeader}); export const countryApi = createApi({ reducerPath: 'countryApi', baseQuery : fetchBaseQuery({ baseUrl : 'https://spott.p.rapidapi.com/' }), endpoints: (builder) =>({ getCity : builder.query({ query: ()=> createRequest('/places/ip/me') }), }) }) export const {useGetCityQuery} = countryApi
weatherApi.js
import {createApi, fetchBaseQuery} from '@reduxjs/toolkit/query/react' const cryptoNewsHeader = { 'x-bingapis-sdk': 'true', 'x-rapidapi-host': 'foreca-weather.p.rapidapi.com', 'x-rapidapi-key': '6e4bb62ec0msh084a4eb0f5c4a3bp149a1ejsn039034dc54d7', }; const createRequest = (url) => ({ url, headers: cryptoNewsHeader}); export const weatherApi = createApi({ reducerPath: 'weatherApi', baseQuery : fetchBaseQuery({ baseUrl : 'https://foreca-weather.p.rapidapi.com/', }), endpoints: (builder) =>({ getSearchLocation : builder.query({ query: (city)=> createRequest(`/location/search/${city}`) }), getCurrentWeatherApi : builder.query({ query: (id) => createRequest(`/current/${id}`)}), getNowCast : builder.query({ query: (id) => createRequest(`/forecast/15minutely/${id}`)}), getWeekCast: builder.query({ query:(id) => createRequest(`/forecast/daily/${id}&?alt=0&tempunit=C&windunit=MS&periods=8&dataset=full`)}) }) }) export const {useGetSearchLocationQuery, useGetCurrentWeatherApiQuery, useGetNowCastQuery, useGetWeekCastQuery} = weatherApi
问题分析与解决方案
1. 无参数变化却触发重复请求的原因
- 查询参数不稳定:App组件中
useGetSearchLocationQuery的参数是city || fetchedCity?.name,当city从undefined变为有效值、或fetchedCity加载完成时,参数会多次变化,触发多次请求。 - 不必要的状态更新:Today组件中,路由参数变化时无条件更新Redux的
city状态,即使值未变化也会触发App组件重渲染,进而引发下游钩子重新请求。 - 默认缓存策略:RTK Query默认缓存仅保留60秒,过期后会自动发起请求;若组件频繁重渲染,缓存过期叠加参数变化会导致请求量激增。
2. 让RTK钩子仅在依赖变化时触发的方案
方案一:稳定查询参数,跳过无效请求
在App组件中优化查询参数逻辑,添加skip选项避免无效请求:
// App.js const { city } = useSelector(state => state.location) const { data: fetchedCity, isLoading: cityLoading } = useGetCityQuery() // 仅当参数有效时才发起请求 const searchQueryParam = city || fetchedCity?.name const { data: locationData, isLoading: locationLoading } = useGetSearchLocationQuery(searchQueryParam, { skip: !searchQueryParam })
方案二:延长缓存时间
修改API切片配置,延长缓存保留时间,减少重复请求:
// weatherApi.js export const weatherApi = createApi({ reducerPath: 'weatherApi', baseQuery: fetchBaseQuery({ baseUrl: 'https://foreca-weather.p.rapidapi.com/' }), // 缓存保留5分钟(默认60秒) keepUnusedDataFor: 300, endpoints: (builder) => ({ // ...端点定义 }) })
方案三:避免不必要的组件重渲染
- 优化Today组件的状态更新逻辑,仅当值变化时才触发dispatch:
// Today.js const currentCity = useSelector(state => state.location.city) useEffect(()=>{ if(city !== currentCity){ dispatch(setCity(city)) } }, [city, currentCity])
- 使用
React.memo包裹SearchComponent、Today等频繁重渲染的组件,减少无意义的钩子执行。
方案四:使用selectFromResult精准获取数据
通过selectFromResult只选择组件需要的部分数据,避免缓存中其他数据变化触发重渲染:
// SearchComponent.js const { data: currentWeatherData, isLoading: isCurrentLoading } = useGetCurrentWeatherApiQuery(locationData?.locations[0]?.id, { selectFromResult: ({ data, isLoading }) => ({ data: data?.current, isLoading }) })
额外优化:处理429错误
在fetchBaseQuery中添加错误拦截,处理请求过多的情况:
// weatherApi.js const baseQuery = fetchBaseQuery({ baseUrl: 'https://foreca-weather.p.rapidapi.com/', fetchFn: async (args, api, extraOptions) => { const result = await fetchBaseQuery({ baseUrl })(args, api, extraOptions) if (result.error?.status === 429) { console.error('请求过于频繁,请稍后再试') // 可添加弹窗提示用户 } return result } }) export const weatherApi = createApi({ reducerPath: 'weatherApi', baseQuery, // ...其他配置 })
内容的提问来源于stack exchange,提问作者Majid
相关产品推荐
相关产品推荐

