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

React应用RTK Query触发429错误:请求重复与触发条件问题

RTK Query 请求过多导致429错误及缓存失效问题

问题描述

在React应用中使用RTK Query从RapidAPI获取天气数据,数据时而能渲染时而失败。失败时控制台报429(请求过多)错误。仅在一处使用RTK Query,但提交表单等操作引发组件重渲染时,RTK钩子会重复发起请求,怀疑是问题根源。

疑问

  1. 单次更新中多次重渲染时,首次重渲染触发RTK请求,后续无参数变化的重渲染应该用缓存数据,为何还会出现429错误?
  2. 能否像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&#39;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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.09 18:15:51