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
- Initial Request: The function first attempts the original request using
rtkBaseQuery
. - 401 Error Detection: If a
401
error occurs (meaning the token is expired), it triggers a refresh request by callingtokenrefresh
. - Token Refresh: If the refresh is successful, the new tokens are saved to Redux using
setCredentials
. - Retry Original Request: The original request is retried with the new access token, ensuring that the user remains authenticated.
- 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:
- Auth Slice: Managed tokens and user session information in the Redux state.
- Base Query Setup: Configured
fetchBaseQuery
to include tokens in the request headers. - Refresh Token Handling: Created
baseQueryWithReauth
to detect expired tokens and refresh them automatically. - 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
Post a Comment