Handling Refresh Tokens in React with Redux Toolkit: A Step-by-Step Guide

In modern web applications, maintaining a secure user session is essential. Token-based authentication with access tokens and refresh tokens is a popular solution, but implementing it properly requires a bit of setup to ensure a smooth experience.

In this blog post, we’ll go over how to handle refresh tokens in a React application using Redux Toolkit. We’ll set up a custom baseQueryWithReauth function that automatically manages the token lifecycle, ensuring users remain authenticated without needing to re-login constantly.

Why Use Access Tokens and Refresh Tokens?

Access tokens are typically short-lived and allow users to access protected resources without needing to log in again. When these tokens expire, refresh tokens (which are longer-lived) allow the app to request a new access token without interrupting the user experience. This approach not only improves security but also enhances usability.

Let’s jump into implementing this in a React app.

Step 1: Setting Up the Redux Slice

First, set up your Redux slice to manage the authentication state, including the access token and refresh token. This setup will enable us to track and update these tokens across the app.

Install @reduxjs/toolkit if you haven’t already npm install @reduxjs/toolkit

Next, define authSlice.js:

import { createSlice } from "@reduxjs/toolkit";

export interface authData {
token:string,
refreshToken: string,
refreshTokenExpiration: string,
}

const initialState = {
token:'',
refreshToken:'',
refreshTokenExpiration:'',

} satisfies authData as authData

const authSlice = createSlice({
name:'auth',
initialState,
reducers:{
setCredentials: (state,action)=>{
state.token = action.payload.token
state.refreshToken = action.payload.refreshToken
state.refreshTokenExpiration = action.payload.refreshTokenExpiration
},
logOut:(state:authData) => {
state.token = ''
state.refreshToken = ''
state.refreshTokenExpiration = ''
}
}
})

export const { setCredentials, logOut } = authSlice.actions
export default authSlice.reducer

Here, we define two main actions:

  • setCredentials: Updates the Redux state with the access token, refresh token, and other relevant data.
  • logOut: Clears the user’s session from the Redux state, effectively logging them out.

Step 2: Configuring BaseQuery for Authenticated Requests

With the Redux slice ready, let’s set up fetchBaseQuery, a helper function from Redux Toolkit, to create our base query with the authorization header.

// src/api/BaseQuery.ts
import { fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { RootState } from '../redux/store';

export const rtkBaseQuery = fetchBaseQuery({
baseUrl: import.meta.env.VITE_API_DOMAIN,
credentials: 'include',
prepareHeaders: (headers, { getState }) => {
const token = (getState() as RootState).auth.token;
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
return headers;
}
});

Step 3: Creating baseQueryWithReauth for Automatic Token Refresh

To handle expired tokens, we’ll create a new base query, baseQueryWithReauth, that automatically attempts to refresh the token whenever a request fails due to 401 Unauthorized.

You can add baseQueryWithReauth in same file where you have the rtkBaseQuery

// src/api/BaseQuery.ts (continue)

export const baseQueryWithReauth: BaseQueryFn<
string | FetchArgs,
any,
FetchBaseQueryError
> = async (args, api, extraOptions) => {
let result = await rtkBaseQuery(args, api, extraOptions)
if (result?.error?.status === 401 && result.error) {
if (!authState.token || !authState.refreshToken) return result;

// Here you can update the refresh token
// You can use any methode alike this one
const refreshToken = (api?.getState() as RootState).auth.refreshToken;
const formData = ({ refreshToken: refreshToken})
const refreshResult = await rtkBaseQuery({
url: 'users/tokenrefresh',
method: 'POST',
body: JSON.stringify(formData),
headers: {
'Content-Type': 'application/json',
}
}, api, extraOptions)

// update Auth state
const data: any = refreshResult;
if (data?.data?.data) {
const user: authData = {
...((api.getState() as RootState).auth),
refreshToken: data.data.data.refreshToken,
token: data.data.data.token,
refreshTokenExpiration: data.data.data.refreshTokenExpiration,
};
api.dispatch(setCredentials(user));

// Continue api call with refreshed token
result = await rtkBaseQuery(args, api, extraOptions);

} else {
api.dispatch(logOut())
}
}
return result
}

How It Works

  1. Initial Request: The function first attempts the original request using rtkBaseQuery.
  2. 401 Error Detection: If a 401 error occurs (meaning the token is expired), it triggers a refresh request by calling tokenrefresh.
  3. Token Refresh: If the refresh is successful, the new tokens are saved to Redux using setCredentials.
  4. Retry Original Request: The original request is retried with the new access token, ensuring that the user remains authenticated.
  5. Logout on Failure: If the refresh fails (e.g., due to an expired or invalid refresh token), the user is logged out by clearing the Redux state.

Step 4: Using baseQueryWithReauth in API Requests

Now that we’ve set up baseQueryWithReauth, we can use it directly in API slices. Here’s an example of how to use it in an API slice definition:

// src/services/apiSlice.js

import { createApi } from '@reduxjs/toolkit/query/react';
import { baseQueryWithReauth } from '../src/api/BaseQuery';

export const apiSlice = createApi({
reducerPath: 'api',
baseQuery: baseQueryWithReauth,
endpoints: (builder) => ({
getUserData: builder.query({
query: () => 'user/profile',
}),
// Other endpoints...
}),
});

export const { useGetUserDataQuery } = apiSlice;

Conclusion

In this guide, we built a custom token refresh solution with Redux Toolkit:

  1. Auth Slice: Managed tokens and user session information in the Redux state.
  2. Base Query Setup: Configured fetchBaseQuery to include tokens in the request headers.
  3. Refresh Token Handling: Created baseQueryWithReauth to detect expired tokens and refresh them automatically.
  4. Effortless Integration: Implemented the solution into an API slice, allowing for transparent token management.

This setup allows your app to handle tokens securely and seamlessly, improving both security and user experience. Try it out and see how it fits into your application!

Comments

Popular posts from this blog

Struggling with Active Links in Next.js? This Hook Solves It!