reactjsredux-toolkit

How to refresh stale data from RTK query?


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;

Solution

  • 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
      }
    );