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
相关产品推荐
相关产品推荐

