Search code examples
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

  • First, I don't know if you can use any hook in your API. You might have to change the way to get the token by:

    1. Have you tried renaming your custom hook with a useGetAccessToken? The error states React Hook names must start with the word "use". This could fix the issue of retrieving the token.
    2. Storing the token in the cookies - my favorite approach.
    3. Passing the token as an argument directy from the state - however I've never done this though.

    For refetching tokens,

    You could try using the baseQueryWithReauth mentioned in the docs, where if the token returns 401 a call to your refreshToken method is made and you can store your updated tokens and retry the request.

    It would look something like this in your implementation:

    import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
    import type {
      BaseQueryFn,
      FetchArgs,
      FetchBaseQueryError,
    } from '@reduxjs/toolkit/query'
    import { Contacts } from "../datamodel/Contact";
    import { getAccessToken } from "../util/userContextManagement";
    
    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;
      },
    });
    
    const baseQueryWithReauth: BaseQueryFn<
      string | FetchArgs,
      unknown,
      FetchBaseQueryError
    > = async (args, api, extraOptions) => {
      let result = await baseQuery(args, api, extraOptions);
    
      if (result.error && result.error.status === 401) {
        // try to get a new token
        const refreshResult = await baseQuery("/refreshToken", api, extraOptions);
    
        if (refreshResult.data) {
          // store the new token in the store or wherever you keep it
          api.dispatch(tokenReceived(refreshResult.data));
          // retry the initial query
          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 const contactsApi = createApi({
      reducerPath: "contactsApi",
      baseQueryWithReauth,
      endpoints: (builder) => ({
        getContacts: builder.query<Contacts, void>({
          query: () => {
            return {
              url: "/contacts",
              method: "GET",
            };
          },
        }),
      }),
    });
    
    export const { useGetContactsQuery } = contactsApi;