I’m building a React application with Redux Toolkit and need to handle token expiration automatically. When my access token expires, the server returns a 403 error, and I want to automatically refresh the token and retry the original request. I have favourites-related async thunks that need authentication. The server expects the access token in the Authorization header. My access token is stored in Redux memory (state.auth.accessToken). Here’s my current code:
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
const url = import.meta.env.VITE_APP_SERVER_URL;
// Thunk to add to favourites
export const addToFavouritesThunk = createAsyncThunk(
'favourites/add',
async (dishId, { rejectWithValue, getState }) => {
try {
const { auth: { accessToken } } = getState();
const response = await axios.post(
`${url}favourites`,
{ dishId },
{
headers: {
'Authorization': `Bearer ${accessToken}`
}
}
);
return response.data;
} catch (error) {
return rejectWithValue(
error.response?.data?.message || 'Failed to add to favourites'
);
}
}
);
// Thunk to get all favourites
export const getFavouritesThunk = createAsyncThunk(
'favourites/getAll',
async (_, { rejectWithValue, getState }) => {
try {
const { auth: { accessToken } } = getState();
const response = await axios.get(
`${url}favourites`,
{
headers: {
'Authorization': `Bearer ${accessToken}`
}
}
);
return response.data;
} catch (error) {
return rejectWithValue(
error.response?.data?.message || 'Failed to get favourites'
);
}
}
);
// Thunk to remove from favourites
export const removeFromFavouritesThunk = createAsyncThunk(
'favourites/remove',
async (dishId, { rejectWithValue, getState }) => {
try {
const { auth: { accessToken } } = getState();
const response = await axios.delete(
`${url}favourites/${dishId}`,
{
headers: {
'Authorization': `Bearer ${accessToken}`
}
}
);
return response.data;
} catch (error) {
return rejectWithValue(
error.response?.data?.message || 'Failed to remove from favourites'
);
}
}
);
// Refresh token thunk (simplified version)
export const refreshTokenThunk = createAsyncThunk(
'auth/refresh',
async (_, { rejectWithValue, getState }) => {
try {
const { auth: { refreshToken } } = getState();
const response = await axios.post(
`${url}auth/refresh`,
{ refreshToken }
);
return response.data; // Contains new accessToken
} catch (error) {
return rejectWithValue(
error.response?.data?.message || 'Token refresh failed'
);
}
}
);
const favouritesSlice = createSlice({
name: 'favourites',
initialState: {
items: [],
loading: false,
error: null,
lastAction: null
},
reducers: {
clearFavouritesError: (state) => {
state.error = null;
},
resetLastAction: (state) => {
state.lastAction = null;
}
},
extraReducers: (builder) => {
builder
// Add to favourites
.addCase(addToFavouritesThunk.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(addToFavouritesThunk.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
state.lastAction = { type: 'add' };
})
.addCase(addToFavouritesThunk.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
})
// Get all favourites
.addCase(getFavouritesThunk.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(getFavouritesThunk.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload.favourites || [];
})
.addCase(getFavouritesThunk.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
})
// Remove from favourites
.addCase(removeFromFavouritesThunk.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(removeFromFavouritesThunk.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
state.lastAction = { type: 'remove' };
})
.addCase(removeFromFavouritesThunk.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
}
});
export const { clearFavouritesError, resetLastAction } = favouritesSlice.actions;
export default favouritesSlice.reducer;
The Problem When the access token expires, my server returns a 403 error. I want to automatically:
Detect 403 errors in any thunk
Call refreshTokenThunk to get a new access token
Retry the original request with the new token
Handle concurrent requests (if multiple requests fail with 403 at the same time)