reactjsreact-reduxrtk-query

How to 'refresh token' using RTK Query


I have a set of API calls written with Redux Toolkit. For example:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { Contacts } from '../datamodel/Contact';
import { getAccessToken } from '../util/userContextManagement';

export const contactsApi = createApi({
    reducerPath: 'contactsApi',
    baseQuery: fetchBaseQuery({ baseUrl: '/api/users/current' }),
    endpoints: builder => ({
        getContacts: builder.query<Contacts, void>({
            query: () => {                
                return ({
                  url: '/contacts',
                  method: 'GET',
                  headers: { Authorization:  `Bearer ${getAccessToken()?.token}`}
                });
              },
        })
    })
})

export const { useGetContactsQuery } = contactsApi

I am able to inject the access token using a function: getAccessToken().

However, I'd like to detect in the function that the access token has expired and refresh it with another API call before the function returns.

Unfortunately, I am not able to do this in this function, because getAccessToken() isn't react hook.

export const getAccessToken = () => {
    const [trigger] = useLazyGetRefreshTokensQuery();
    (...)
    return getUserContext().tokens.find(t => t.type === TokenTypeEnum.ACCESS_TOKEN)
}

I am getting:

React Hook "useLazyGetRefreshTokensQuery" is called in function "getAccessToken" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"

How could I refresh the token in RTK Query?


Solution

  • Expanding on Fer Toasted's answer, you can create a helper function for general use like this:

    
        import type { BaseQueryFn, FetchArgs, FetchBaseQueryError } from '@reduxjs/toolkit/query'
        import { getRefreshedToken } from './msal/state'
        import { tokenReceived } from './msal/reducers'
        
        const getBaseQueryWithReauth = (
          baseQuery: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError>
        ): BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryError> => {
          return async function (args, api, extraOptions) {
            let result = await baseQuery(args, api, extraOptions)
        
            if (result.error && (result.error.status === 401 || result.error.data === 'Unauthorized')) {
              const token = await getRefreshedToken()
        
              if (token) {
                api.dispatch(tokenReceived(token))
                result = await baseQuery(args, api, extraOptions)
              } else {
                // refresh failed - do something like redirect to login or show a "retry" button
                // api.dispatch(loggedOut())
              }
            }
            return result
          }
        }
        
        export default getBaseQueryWithReauth
    
    

    Then in your RTK query:

    import getBaseQueryWithReauth
     from './get-base-query-with-reauth.js'
    
    const baseQuery = fetchBaseQuery({
      baseUrl: '/api/users/current',
      prepareHeaders: (headers) => {
        // this method should retrieve the token without a hook
        const token = getAccessToken();
    
        if (token) {
          headers.set("authorization", `Bearer ${token}`);
        }
        return headers;
      },
    });
    
    export const contactsApi = createApi({
      reducerPath: "contactsApi",
      baseQuery: getBaseQueryWithReauth(baseQuery),
      endpoints: (builder) => ({
        getContacts: builder.query<Contacts, void>({
          query: () => {
            return {
              url: "/contacts",
              method: "GET",
            };
          },
        }),
      }),
    });