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

React-Redux鉴权缓存异常:切换用户仍显示旧数据求助

问题:RTK Query 切换用户后数据缓存未更新

我用Redux Toolkit Query实现了React-Redux鉴权,从Node后端获取数据,后端返回accessToken。现在遇到棘手问题:首次登录正常,同一电脑登出换其他用户登录时,新用户获取数据的请求不发往后端,直接显示前一个用户的数据。已经在登出时清空了Redux状态,试了各种清缓存的方法都没用,卡壳了。


相关代码

apiSlice.js

import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

const BASE_URL = "http://localhost:10000/api/v1"

const baseQuery = fetchBaseQuery({
    baseUrl: `${BASE_URL}/user`,
    credentials: 'include',
    prepareHeaders: (headers, { getState }) => {
        const token = getState().user.token
        console.log(token)
        if (token) {
            headers.set("Authorization", `Bearer ${token}`)
        }
        return headers
    }
})

const baseQueryWithReauth = async (args, api, extraOptions) => {
    let result = await baseQuery(args, api, extraOptions);
    console.log(result)
    if (result?.error?.originalStatus === 403) {
        //send refresh token to get new access token
        const refreshResult = await baseQuery('/token', api, extraOptions);
        console.log(refreshResult)
        if (refreshResult?.data) {
            api.dispatch(setCredentials({ ...refreshResult.data }));
            result = await baseQuery(args, api, extraOptions);
        } else {
            api.dispatch(logOut());
        }
    }


    return result;
}

export const apiSlice = createApi({
    reducerPath: 'authSlice',
    baseQuery: baseQueryWithReauth,
    tagTypes: ['User'],
    endpoints: builder => ({}),
    keepUnusedDataFor: 0,
})

authApi.js

import { apiSlice } from "./apiSlice";
import { userApi } from "./userApi";


export const authApi = apiSlice.injectEndpoints({
    endpoints: (builder) => ({
        registerUser: builder.mutation({
            query: (user) => ({
                url: `/signup`,
                method: "POST",
                body: user
            })
        }),
        loginUser: builder.mutation({
            query: (data) => ({
                url: '/login',
                method: 'POST',
                body: data,
                credentials: 'include'
            }),
            providesTags: ['User']
        }),
        logoutUser: builder.mutation({
            query: () => ({
                url: "/logout",
                method: 'POST',
                credentials: "include"
            }),
            async onQueryStarted(args, { dispatch, queryFulfilled }) {
                try {
                    await queryFulfilled
                } catch (error) {

                }
            }
        })
    })
})
export const {
    useRegisterUserMutation,
    useLoginUserMutation,
    useLogoutUserMutation
} = authApi;

userApi.js

import { apiSlice } from "./apiSlice";

export const userApi = apiSlice.injectEndpoints({
    endpoints: (builder) => ({
        getMe: builder.query({
            query: () => ({
                url: `/me?_=${new Date().getTime()}`,
                credentials: "include",
            }),
            transformResponse: (result) => result.data.user,
            async onQueryStarted(arg, { dispatch, queryFulfilled }) {
                try {
                    const { data } = await queryFulfilled;
                    dispatch(setUser(data));
                } catch (error) { }
            },
        }),
    }),
})

export const { useGetMeQuery } = userApi

userSlice.js

import { createSlice } from '@reduxjs/toolkit';


const initialState = {
    user: null,
    token: null
};

const userSlice = createSlice({
    initialState,
    name: 'userSlice',
    reducers: {
        setCredentials: (state, action) => {
            const { accessToken, data } = action.payload
            state.token = accessToken
            state.user = data;
        },
        logout: () => initialState,
        setUser: (state, action) => {
            state.user = action.payload;
        },
    },
});

export default userSlice;

export const userActions = userSlice.actions;

store.js

import { combineReducers, configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/dist/query";
import myListSlice from "./features/myListSlice";
import { persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER } from "redux-persist";
import storage from "redux-persist/lib/storage";
import userSlice from "./features/userSlice";
import { apiSlice } from "./api/apiSlice";


const persistConfig = {
    key: "root",
    storage: storage
}
const rootReducer = combineReducers({
    myList: myListSlice.reducer,
    user: userSlice.reducer,
    [apiSlice.reducerPath]: apiSlice.reducer,
})
const persistedReducer = persistReducer(persistConfig, rootReducer)
export const store = configureStore({
    reducer: persistedReducer,
    middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware({ serializableCheck: { ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], } }).concat(apiSlice.middleware)
})

setupListeners(store.dispatch)

Login.jsx(调度相关代码)

import React, { useEffect } from "react";
import { Link, useActionData, useNavigate, useSubmit } from "react-router-dom";
import Logo from "../../assets/images/Logo.svg";
import background from "../../assets/images/background.jpg";
import { useFormik } from "formik";
import { loginValidate } from "../../Validations/UserValidation";
import { useLoginUserMutation } from "../../Store/api/authApi";
import { useDispatch } from "react-redux";
import { userActions } from "../../Store/features/userSlice";
import Spinner from "../../Components/Spinner";

const Login = () => {
  // const loading = false;
  const [loginUser, { isLoading: loading }] = useLoginUserMutation();
  const submit = useSubmit();
  const navigate = useNavigate();
  const data = useActionData();
  const { setCredentials } = userActions;
  const dispatch = useDispatch();
  const formik = useFormik({
    initialValues: {
      email: "",
      password: "",
    },
    validationSchema: loginValidate,
    onSubmit: (values) => {
      submit(values, { method: "post" });
    },
  });
  useEffect(() => {
    const fetchData = async () => {
      if (data) {
        try {
          const { accessToken } = await loginUser(data).unwrap();
          dispatch(setCredentials({ accessToken, data }));
          navigate("/");
        } catch (error) {
          console.error(error);
        }
      }
    };

    fetchData();
  }, [data]);

Root.jsx

import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Outlet, useNavigate } from "react-router-dom";
import Footer from "../Components/Footer";
import Navbar from "../Components/Navbar";
import Spinner from "../Components/Spinner";
import { useGetMeQuery } from "../Store/api/userApi";
import { userActions } from "../Store/features/userSlice";

function Root() {
  const { token } = useSelector((state) => state.user);
  const dispatch = useDispatch();
  const { setUser } = userActions;
  const { data: user, isLoading } = useGetMeQuery();
  const navigate = useNavigate();
  useEffect(() => {
    if (token === null) {
      navigate("/account");
    }
    dispatch(setUser(user));
  }, []);

  return (
    <>
      {isLoading ? (
        <>
          <Spinner />
        </>
      ) : (
        <div className="bg-black">
          <Navbar user={user} />
          <div className="flex flex-wrap items-center justify-between">
            <Outlet />
          </div>
          <Footer />
        </div>
      )}
    </>
  );
}

export default Root;

解决方案

1. 登出时主动失效RTK Query缓存标签

RTK Query的缓存通过标签管理,登出时需要主动使User标签失效,触发重新请求。修改authApi.js中的logoutUser mutation:

logoutUser: builder.mutation({
    query: () => ({
        url: "/logout",
        method: 'POST',
        credentials: "include"
    }),
    async onQueryStarted(args, { dispatch, queryFulfilled }) {
        try {
            await queryFulfilled;
            // 失效User标签,清空相关缓存
            dispatch(apiSlice.util.invalidateTags(['User']));
            // 同步清空用户状态
            dispatch(userActions.logout());
        } catch (error) {
            console.error(error);
        }
    }
})

2. 给getMe查询关联User标签

确保getMe查询与User标签绑定,这样标签失效时会自动重新请求。修改userApi.js:

getMe: builder.query({
    query: () => ({
        url: `/me`, // 可移除时间戳,RTK Query会自动处理缓存失效
        credentials: "include",
    }),
    transformResponse: (result) => result.data.user,
    providesTags: ['User'], // 添加这行
    async onQueryStarted(arg, { dispatch, queryFulfilled }) {
        try {
            const { data } = await queryFulfilled;
            dispatch(setUser(data));
        } catch (error) { }
    },
}),

3. 修复Redux Persist的持久化范围

当前store持久化了整个rootReducer,包括RTK Query的缓存数据。可选择两种方案:

  • 方案一:登出时清除持久化缓存
    在logoutUser的onQueryStarted中添加:
import { persistStore } from 'redux-persist';
import { store } from '../store';

const persistor = persistStore(store);

// 在登出逻辑中添加
persistor.purge();
  • 方案二:排除RTK Query的reducer不被持久化
    修改store.js中的persistConfig:
const persistConfig = {
    key: "root",
    storage: storage,
    whitelist: ['myList', 'user'] // 只持久化需要的reducer,排除RTK Query的缓存
}

4. 修正Root.jsx的useEffect依赖

原代码中useEffect依赖为空数组,导致token变化后不会重新执行导航逻辑。修改如下:

useEffect(() => {
    if (token === null) {
        navigate("/account");
    }
}, [token, navigate]); // 添加依赖

// 移除手动dispatch(setUser(user)),直接使用useGetMeQuery返回的data即可

5. 登录成功后强制刷新用户数据

在Login.jsx的登录成功回调中,手动触发User标签失效,确保获取最新用户数据:

// 登录成功后添加
import { userApi } from "../../Store/api/userApi";

dispatch(userApi.util.invalidateTags(['User']));

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 07:24:54