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?
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",
};
},
}),
}),
});