In modern applications, implementing secure and seamless authentication mechanisms is crucial. One common approach is using access tokens with a refresh token mechanism. This article demonstrates how to integrate an automatic token refresh workflow using Axios, RTK Query, and Zustand for state management in a React Native app.
Overview of the Implementation
• Axios Interceptors: Intercepts requests and responses to add authentication tokens and handle token expiration.
• Zustand: Manages the authentication state, including storing and updating tokens.
• RTK Query: Handles API calls, including a mutation for refreshing the token.
Let’s start step by step…
1. Prerequisites
Before we dive into the code, ensure you have the required dependencies installed in your project. Use the following commands to install the necessary packages:
npm install @reduxjs/toolkit react-redux axios axios-logger @react-native-async-storage/async-storage zustand
2. Configuring Axios with Token Handling
Axios interceptors are used to add the token to outgoing requests and handle token expiration by refreshing it.
AxiosInstance.ts
import axios from 'axios';
import * as AxiosLogger from 'axios-logger';
import { expoMuseumApi } from '@/src/redux/RTKQuery';
import { useAppStore } from '@/src/zustand/useAppStore';
import configureAppStore from '@/src/store/Store';
const axiosInstance = axios.create();
axiosInstance.interceptors.request.use((config) => {
const token = useAppStore.getState().token;
// Add bearer token to headers
config.headers.Authorization = `Bearer ${token}`;
// Keep the original logger
AxiosLogger.requestLogger(config);
console.log('Authorization header', config.headers);
return config;
}, AxiosLogger.errorLogger);
axiosInstance.interceptors.response.use(
AxiosLogger.responseLogger,
AxiosLogger.errorLogger
);
axiosInstance.interceptors.response.use(
AxiosLogger.responseLogger, // Log response
async (error) => {
const originalRequest = error.config;
// Handle 401 (Unauthorized) errors
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true; // Prevent infinite retry loops
try {
// Trigger the RTK Query `postRefreshToken` mutation
const { data } = await configureAppStore().dispatch(
expoMuseumApi.endpoints.postRefreshToken.initiate()
);
// Update token in Zustand store
useAppStore.getState().saveToken(data as string);
// Update Authorization header with the new token
originalRequest.headers.Authorization = `Bearer ${useAppStore.getState().getToken()}`;
// Retry the original request
return axiosInstance(originalRequest);
} catch (refreshError) {
console.error('Failed to refresh token:', refreshError);
// Clear user data and token in Zustand store on failure
useAppStore.getState().clearToken();
//TODO [Navigate to login screen]
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
export default axiosInstance;
3. Setting Up Zustand for Token Management
We use Zustand to manage the token state. The token is persisted locally using AsyncStorage to ensure it’s retained across app sessions.
useAppStore.ts
import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import AsyncStorage from '@react-native-async-storage/async-storage';
interface zustandConfig {
token: string;
saveToken: (token: string) => void;
getToken: () => string;
clearToken: () => void;
}
export const useAppStore = create<zustandConfig>()(
persist(
(set, get) => ({
token: '',
saveToken: (token: string) => set({ token }),
getToken: () => get().token,
clearToken: () => set({ token: '' }),
}),
{
name: '@expoMuseum',
storage: createJSONStorage(() => AsyncStorage),
}
)
);
4. RTK Query with Axios Base Query
Define a base query using Axios to integrate it with RTK Query.
BaseQuery.ts
import { AxiosError, AxiosRequestConfig } from 'axios';
import { BaseQueryFn } from '@reduxjs/toolkit/query/react';
import axiosInstance from '@/src/redux/AxiosInstance';
import { AppConstants } from '@/src/constants/AppConstant';
type AxiosBaseQueryArgs = {
baseUrl?: string;
};
type AxiosQueryParams = {
url: string;
method?: AxiosRequestConfig['method'];
data?: AxiosRequestConfig['data'];
params?: AxiosRequestConfig['params'];
headers?: AxiosRequestConfig['headers'];
};
const axiosBaseQuery =
(
{ baseUrl }: AxiosBaseQueryArgs = { baseUrl: AppConstants.BASE_URL }
): BaseQueryFn<AxiosQueryParams, unknown, unknown> =>
async ({ url, method = 'GET', data, params, headers }) => {
try {
const result = await axiosInstance({
url: baseUrl + url,
method,
data,
params,
headers,
});
return { data: result.data };
} catch (axiosError) {
const error = axiosError as AxiosError;
return {
error: {
status: error.response?.status || 500,
data: error.response?.data || error.message,
},
};
}
};
export default axiosBaseQuery;
5. RTK Query API Definition
Define an API slice that includes a mutation for refreshing tokens and other queries.
RTKQuery.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { useAppStore } from '@/src/zustand/useAppStore';
import axiosBaseQuery from '@/src/redux/BaseQuery';
export const expoMuseumApi = createApi({
reducerPath: 'expoMuseumApi',
baseQuery: axiosBaseQuery(),
endpoints: (builder) => ({
postRefreshToken: builder.mutation<string, void>({
query: () => ({
url: 'auth/refreshToken',
method: 'POST',
headers: {
// Pass the refreshToken in the header while calling refresh token api
Authorization: `Bearer ${useAppStore().token}`,
},
}),
transformResponse: (response: string) => response,
}),
}),
});
export const { usePostRefreshTokenMutation } = expoMuseumApi;
6. Setting Up Redux Store
To integrate RTK Query into your app, you need to configure the Redux store and attach the API middleware. This step ensures that RTK Query manages the caching and lifecycle of API calls.
Store.ts
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { expoMuseumApi } from '@/src/redux/RTKQuery';
const configureAppStore = () => {
const store = configureStore({
reducer: {
[expoMuseumApi.reducerPath]: expoMuseumApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
immutableCheck: false,
}).concat([expoMuseumApi.middleware]),
devTools: process.env.NODE_ENV === 'development',
});
setupListeners(store.dispatch);
return store;
};
export default configureAppStore;
Ref: https://github.com/piashcse/expo-museum
Medium: https://piashcse.medium.com/authenticator-in-axios-with-rtk-query-in-a-react-native-application-c9610c8e288d