How to automatically refresh access token when receiving 403 errors in Redux Toolkit async thunks?

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)