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

RTK Query线上部署后缓存不更新,本地运行正常

问题: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

排查建议

  1. 检查生产环境API响应的缓存控制头
    服务器可能返回了强缓存头(如Cache-Control: max-age=86400),导致浏览器直接读取本地缓存,跳过实际API请求。可以:

    • 在fetchBaseQuery中添加cache: 'no-store'禁用浏览器缓存:
      const baseQuery = fetchBaseQuery({
          // ...其他配置
          cache: 'no-store'
      });
      
    • 要求后端调整响应头,设置Cache-Control: no-cache, must-revalidate,确保浏览器每次都验证缓存有效性。
  2. 验证标签匹配的准确性
    确认providesTags和invalidatesTags中的标签名称大小写、拼写完全一致(比如'Parcel'和'parcel'会被视为不同标签)。虽然本地正常,但生产环境的序列化/解析逻辑可能存在差异,需仔细核对。

  3. 调试生产环境的RTK Query缓存状态
    开启Redux DevTools的生产环境支持,查看缓存中的数据和标签状态:

    • 确认mutation执行后,对应标签是否被标记为失效
    • 检查是否触发了重新获取请求,以及请求的响应数据是否正确
  4. 修正refetch的调用时机
    当前代码中await issueParcel(parcelID)后直接调用refetch(),但未确认mutation是否成功。修改为:

    const result = await issueParcel(parcelID);
    if (result.data && !result.error) {
      refetch();
    }
    

    确保仅在mutation成功时触发refetch,避免错误情况下的无效操作。

  5. 检查transformResponse的逻辑
    getParcels的transformResponse中使用固定的initialState,可能导致生产环境中数据更新不彻底。改为动态创建初始状态:

    transformResponse: responseData => {
        return parcelsAdapter.setAll(parcelsAdapter.getInitialState(), responseData)
    },
    

    避免复用同一个初始状态对象导致的引用问题。

  6. 确认生产环境的baseURL配置
    检查baseUrlPro是否正确指向生产服务器,确保mutation和query请求的是同一个服务器,避免跨域或路径错误导致的数据不同步。


内容的提问来源于stack exchange,提问作者japheth

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 08:25:27