RTK Query线上部署后缓存不更新,本地运行正常
开发数据密集型应用,集成RTK Query后遇到以下问题:
- 本地环境中,执行mutation后标签失效(tag invalidation)功能正常,缓存会更新并同步UI
- 部署到服务器后,API调用成功且服务器数据已变更,但缓存始终不更新,UI显示旧数据
- 硬刷新页面无效,必须手动清除浏览器缓存才能看到最新数据
- 已设置
refetchOnFocus: true和refetchOnMountOrArgChange: 5,但完全无效,缓存超过30分钟也不会重新获取 - 尝试在mutation成功后调用
refetch(),线上环境仍无效果
示例场景:调用issueParcel mutation发放包裹,服务器中包裹状态变为已送达,但UI仍显示旧状态。
相关代码
store.js
import { configureStore } from "@reduxjs/toolkit"; import { apiSlice } from '../api/apiSlice'; import authReducer from "./auth/authSlice"; export const store = configureStore({ reducer: { [apiSlice.reducerPath]: apiSlice.reducer, auth: authReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(apiSlice.middleware) });
apiSlice.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import { Mutex } from 'async-mutex'; import { logout, setCredentials } from '../features/auth/authSlice'; import { baseUrlDev, baseUrlPro } from './baseUrl'; const baseURL = window.location.hostname.includes("dev") || window.location.hostname.includes("localhost") ? baseUrlDev : baseUrlPro; const mutex = new Mutex(); const baseQuery = fetchBaseQuery({ baseUrl: baseURL, credentials: 'include', timeout: 15000, prepareHeaders: (headers, {getState}) => { const token = getState().auth.token || JSON.parse(localStorage.getItem("authenticatedUser"))?.accessToken; if (token) { headers.set("Authorization", `Bearer ${token}`) } return headers; } }); const baseQueryWithReauth = async (args, api, extraOptions) => { await mutex.waitForUnlock(); let result = await baseQuery(args, api, extraOptions) if (result?.error?.originalStatus === 403) { if (!mutex.isLocked()) { const release = await mutex.acquire(); try { console.log('sending refresh token'); // send refresh token to get a new access token const refreshResult = await baseQuery('/auth/refresh', api, extraOptions); // console.log(refreshResult); if(refreshResult?.data) { const email = api.getState().auth.email || JSON.parse(localStorage.getItem("authenticatedUser"))?.email; const role = api.getState().auth.role || JSON.parse(localStorage.getItem("authenticatedUser"))?.role; const name = api.getState().auth.name || JSON.parse(localStorage.getItem("authenticatedUser"))?.name; // store the new token api.dispatch(setCredentials({ accessToken: refreshResult.data.accessToken, email, role, name, branch: refreshResult.data.branch, company: refreshResult.data.company })); // retry the original query with new access token result = await baseQuery(args, api, extraOptions); } else { await baseQuery('/auth/logout', api, extraOptions); api.dispatch(logout()); } } finally { release(); } } else { await mutex.waitForUnlock(); result = await baseQuery(args, api, extraOptions); } } return result; } export const apiSlice = createApi({ baseQuery: baseQueryWithReauth, tagTypes: [ 'Branch', 'Company', 'Customer', 'Driver', 'Parcel', 'ParcelTransaction', 'ParcelType', 'Staff', 'Town', 'TransactionChannel', 'User', 'VehicleOwner', 'Vehicle', 'VehicleType' ], refetchOnMountOrArgChange: 5, refetchOnFocus: true, endpoints: builder => ({}) })
parcelSlice.js
import { createSelector, createEntityAdapter } from "@reduxjs/toolkit"; import { apiSlice } from '../../api/apiSlice'; const SLICE_URL = '/parcels'; const parcelsAdapter = createEntityAdapter({ sortComparer: (a, b) => b.parcelCode.localeCompare(a.parcelCode) }) const initialState = parcelsAdapter.getInitialState() export const parcelApiSlice = apiSlice.injectEndpoints({ endpoints: builder => ({ getParcels: builder.query({ query: () => `${SLICE_URL}`, transformResponse: responseData => { return parcelsAdapter.setAll(initialState, responseData) }, providesTags: (result, error, id) => ['Parcel', 'ParcelTransaction'] }), getParcel: builder.query({ query: (id) => `${SLICE_URL}/${id}`, providesTags: (result, error, id) => ['Parcel', 'ParcelTransaction'], }), getParcelsAvailableForDispatch: builder.query({ query: () => `${SLICE_URL}/available-for-dispatch`, providesTags: (result, error, id) => ['Parcel', 'ParcelTransaction'], }), getParcelsAssignedToVehicle: builder.query({ query: (vehicleID) => `${SLICE_URL}/assigned-to-vehicle/?vehicleID=${vehicleID}`, providesTags: (result, error, id) => ['Parcel', 'ParcelTransaction'], }), getParcelsAwaitingRecipients: builder.query({ query: () => `${SLICE_URL}/parcels-awaiting-recipients`, providesTags: (result, error, id) => ['Parcel', 'ParcelTransaction'], }), addParcel: builder.mutation({ query: parcelData => ({ url: `${SLICE_URL}`, method: 'POST', body: { ...parcelData } }), invalidatesTags: ['Parcel', 'ParcelTransaction'] }), issueParcel: builder.mutation({ query: parcelID => ({ url: `${SLICE_URL}/issue-parcel/${parcelID}`, method: 'PATCH', body: { id: parcelID } }), invalidatesTags: ['Parcel', 'ParcelTransaction'] }), updateParcel: builder.mutation({ query: ({id, parcelData}) => ({ url: `${SLICE_URL}/${id}`, method: 'PATCH', body: { ...parcelData } }), invalidatesTags: (result, error, arg) => [ { type: 'Parcel', id: arg.id } ] }), }) }); export const { useGetParcelsQuery, useGetParcelQuery, useGetParcelsAvailableForDispatchQuery, useGetParcelsAssignedToVehicleQuery, useGetParcelsAwaitingRecipientsQuery, useAddParcelMutation, useIssueParcelMutation, useUpdateParcelMutation, } = parcelApiSlice; // returns the query result object export const selectParcelsResult = parcelApiSlice.endpoints.getParcels.select(); // Creates memoized selector const selectParcelsData = createSelector( selectParcelsResult, parcelsResult => parcelsResult.data // normalized state object with ids & entities ); //getSelectors creates these selectors and we rename them with aliases using destructuring export const { selectAll: selectAllParcels, selectById: selectParcelById, selectIds: selectParcelIds // Pass in a selector that returns the parcels slice of state } = parcelsAdapter.getSelectors(state => selectParcelsData(state) ?? initialState)
parcelDetails.jsx
const ParcelDetails = () => { const { id } = useParams(); const navigate = useNavigate(); const theme = useTheme(); const { data: parcelDetails, isLoading, isError, error, refetch } = useGetParcelQuery(id); const [issueParcel] = useIssueParcelMutation() const authenticatedUser = JSON.parse(localStorage.getItem("authenticatedUser")); const breadcrumbs = [ { name: "Parcel", path: "/parcel" }, { name: parcelDetails?.parcelCode } ]; const staffName = parcelDetails?.staff?.ownuser?.firstName + " " + parcelDetails?.staff?.ownuser?.lastName; const handleIssueParcel = (parcelID) => { Swal.fire({ title: 'Are you sure you want to issue this parcel?', html: '<p>ParcelCode: ' + parcelDetails.parcelCode + '</p> <br />', icon: 'warning', showCancelButton: true, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: 'Yes, issue Parcel!' }).then(async (result) => { if (result.isConfirmed) { try { Swal.fire({ title: "Issuing Parcel", html: "Please wait..." }) Swal.showLoading() await issueParcel(parcelID); refetch(); Swal.hideLoading() Swal.fire(`parcel ${parcelDetails.parcelCode} issued successfully!`, '', 'success'); } catch (error) { console.log(error); } } else if (result.isDenied) { Swal.fire('Parcel not issued.', '', 'info'); } }) } return (<>Parcel Display UI</>) } export default ParcelDetails
排查建议
检查生产环境API响应的缓存控制头
服务器可能返回了强缓存头(如Cache-Control: max-age=86400),导致浏览器直接读取本地缓存,跳过实际API请求。可以:- 在
fetchBaseQuery中添加cache: 'no-store'禁用浏览器缓存:const baseQuery = fetchBaseQuery({ // ...其他配置 cache: 'no-store' }); - 要求后端调整响应头,设置
Cache-Control: no-cache, must-revalidate,确保浏览器每次都验证缓存有效性。
- 在
验证标签匹配的准确性
确认providesTags和invalidatesTags中的标签名称大小写、拼写完全一致(比如'Parcel'和'parcel'会被视为不同标签)。虽然本地正常,但生产环境的序列化/解析逻辑可能存在差异,需仔细核对。调试生产环境的RTK Query缓存状态
开启Redux DevTools的生产环境支持,查看缓存中的数据和标签状态:- 确认mutation执行后,对应标签是否被标记为失效
- 检查是否触发了重新获取请求,以及请求的响应数据是否正确
修正refetch的调用时机
当前代码中await issueParcel(parcelID)后直接调用refetch(),但未确认mutation是否成功。修改为:const result = await issueParcel(parcelID); if (result.data && !result.error) { refetch(); }确保仅在mutation成功时触发refetch,避免错误情况下的无效操作。
检查transformResponse的逻辑
getParcels的transformResponse中使用固定的initialState,可能导致生产环境中数据更新不彻底。改为动态创建初始状态:transformResponse: responseData => { return parcelsAdapter.setAll(parcelsAdapter.getInitialState(), responseData) },避免复用同一个初始状态对象导致的引用问题。
确认生产环境的baseURL配置
检查baseUrlPro是否正确指向生产服务器,确保mutation和query请求的是同一个服务器,避免跨域或路径错误导致的数据不同步。
内容的提问来源于stack exchange,提问作者japheth

