Here's piece of my code. I added refetchOnMountOrArgChange and now I see how it fetches new data on network but the data from query is still stale and I get previous user data and that's why it exits because of role mismatch. Adding providesTags and invalidatesTags didn't help either.
const ProtectedRoute = ({ redirectPath = '/', role }: IProtectedRouteProps) => {
const exit = () => {
dispatch(logout());
navigate(redirectPath, { replace: true });
};
const { data, // STALE DATA
isError, isLoading} = useGetUserDetailsQuery('userDetails', {
refetchOnMountOrArgChange: true,
pollingInterval: 30000
});
useEffect(() => {
if (isError) {
exit();
} else if (data && Object.keys(data).length > 0) {
dispatch(setCredentials(data));
const userRole = data?.organization?.role;
if (userRole && role && userRole !== role) {
exit();
}
}
}, [data, isError, role]);
return <div>Protected Content</div>;
};
export default ProtectedRoute;
Here's my base api
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { baseApiUrl } from 'configs/env-exports';
export const baseApi = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
// base url of backend API
baseUrl: baseApiUrl,
prepareHeaders: (headers) => {
const token = localStorage.getItem('userToken');
if (token) {
headers.set('authorization', token);
return headers;
}
},
}),
endpoints: (builder) => ({
getUserDetails: builder.query({
query: () => ({
url: 'api/v1/account/info',
method: 'GET',
}),
}),
}),
});
export const {
useGetUserDetailsQuery,
} = baseApi;
and my slice which i minimized removing unnecessary parts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// initialize userToken from local storage
export const userToken = localStorage.getItem('userToken')
? localStorage.getItem('userToken')
: null;
export type IUserInfo = null | {
id: number;
login: string;
name: string;
notifications: {
id: number;
type: string;
value: string;
}[];
organization: {
name: string;
address: string;
role: string;
};
};
type AuthApiState = {
userInfo: IUserInfo;
userToken: null | string;
userRole: null | string;
};
const initialState: AuthApiState = {
userInfo: null,
userToken,
userRole: null,
};
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
logout: (state) => {
state.userToken = null;
state.userInfo = null;
state.userRole = null;
localStorage.removeItem('userToken');
},
setCredentials: (state, { payload }) => {
state.userInfo = payload;
state.userRole = payload?.organization?.role;
},
},
});
export const { logout, setCredentials } = authSlice.actions;
When I logout I want my data from query to be null or undefined instead of showing cached response
You could dispatch the resetApiState action to:
manually reset the api state completely. This will immediately remove all existing cache entries, and all queries will be considered 'uninitialized'.
Note however the docs also warn:
Note that hooks also track state in local component state and might not fully be reset by resetApiState.
I suspect there are possible edge cases where this happens, but in most cases I've seen it just works and clears the API slice state.
I suggest converting your logout
action into a Thunk so that it can also dispatch additional actions where the authSlice
can handle the action in its extraReducers
. Since reducer functions are to be considered pure functions the updating of localStorage should be moved out of the reducer anyway.
Example:
import { createAsyncThunk } from '@reduxjs/toolkit';
import { baseApi } from '../path/to/baseApi';
export const logout = createAsyncThunk(
"auth/logout",
(_, { dispatch }) => {
localStorage.removeItem('userToken');
dispatch(baseApi.util.resetApiState());
}
);
import { createSlice, isAnyOf, PayloadAction } from '@reduxjs/toolkit';
import { logout } from '../path/to/logout';
...
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setCredentials: (state, { payload }) => {
state.userInfo = payload;
state.userRole = payload?.organization?.role;
},
},
extraReducers: builder => {
builder.addMatcher(
isAnyOf(logout.fulfilled, logout.rejected),
(state) => {
state.userToken = null;
state.userInfo = null;
state.userRole = null;
}
);
},
});
export const { setCredentials } = authSlice.actions;
export default authSlice.reducer;
I also would like to point out that your getUserDetails
endpoint doesn't consume any arguments
getUserDetails: builder.query({
query: (/* no argument */) => ({
url: 'api/v1/account/info',
method: 'GET',
}),
}),
So passing any value to query hooks is irrelevant. Typically you'd just pass nothing, or just undefined
in the case that you need to pass the second options argument.
const {
data,
isError,
isLoading
} = useGetUserDetailsQuery(
undefined, // no argument
{
refetchOnMountOrArgChange: true,
pollingInterval: 30000
}
);