Обновите токен доступа с помощью набора инструментов redux-toolkit asyncThunks

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axiosInstance from "../utils/axiosInstance";

export const logoutUser = createAsyncThunk(
  "users/logoutUser",
  async (_, { rejectWithValue, dispatch }) => {
    try {
      const response = await axiosInstance.post("/users/logout");
      return response.data;
    } catch (error) {
      console.info(error.response.status);
      if (error.response.status === 401) {
        try {
          await dispatch(refreshUserToken());

          const retryResponse = await dispatch(logoutUser());
          return retryResponse;
        } catch (refreshError) {
          return rejectWithValue(refreshError.message);
        }
      } else {
        return rejectWithValue(error.message);
      }
    }
  }
);

export const refreshUserToken = createAsyncThunk(
  "users/refreshUserToken",
  async (_, { rejectWithValue }) => {
    try {
      const response = await axiosInstance.post("/users/refresh-token");
      console.info("refreshToken->", response.data);
      return response.data;
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

const initialState = {
  user: {},
  status: "idle",
  error: null,
};

const userSlice = createSlice({
  name: "user",
  initialState,
  reducers: {},
  extraReducers(builder) {
    builder
      .addCase(logoutUser.pending, (state, action) => {
        state.status = "loading";
      })
      .addCase(logoutUser.fulfilled, (state, action) => {
        state.status = "success";
        console.info("case f logout->", action.payload); // log statement
        state.user = {};
      })
      .addCase(logoutUser.rejected, (state, action) => {
        state.error = action.payload;
      })
      .addCase(refreshUserToken.pending, (state, action) => {
        state.status = "loading";
      })
      .addCase(refreshUserToken.fulfilled, (state, action) => {
        state.status = "success";
        console.info("case f refreshToken->", action.payload); // log statement
      })
      .addCase(refreshUserToken.rejected, (state, action) => {
        state.error = action.payload;
      });
  },
});

export const getUserState = (state) => state.user.status;
export const getUserError = (state) => state.user.error;
export const getUser = (state) => state.user.user;

export const {} = userSlice.actions;

export default userSlice.reducer;

ниже приведен мой экземпляр axios.

const axiosInstance = axios.create({
    baseURL: "http://localhost:8082/api",
    withCredentials: true
});

Я новичок в Redux-Toolkit, поэтому я в замешательстве. В logoutUser asyncThunk я проверяю, равен ли полученный мной код ошибки 401 (истёк срок действия токена доступа), затем я делаю запрос на обновление токена, вызывая refreshUserToken thunk внутри logoutUser thunk, но это приводит к тому, что logoutThunk пробежаться дважды. Как я могу это исправить? Или есть лучший способ обработки обновления токена с помощью asyncThunks?

Примечание. Я использую файлы cookie для отправки и получения токена доступа и обновления с сервера.

🤔 А знаете ли вы, что...
JavaScript можно использовать для манипуляции DOM (Document Object Model), что позволяет изменять содержимое и структуру веб-страницы.


2
88
1

Ответ:

Решено

Вместо повторной отправки действия logoutUser, которое запускает действие Thunk хотя бы еще один раз, вы можете повторно вызвать только конечную точку.

Пример:

export const logoutUser = createAsyncThunk(
  "users/logoutUser",
  async (_, { rejectWithValue, dispatch }) => {
    try {
      const { data } = await axiosInstance.post("/users/logout");
      return data;
    } catch(error) {
      console.info(error.response.status);
      if (error.response.status === 401) {
        try {
          // Attempt token refresh, unwrap result
          await dispatch(refreshUserToken()).unwrap();

          // Retry original request
          const { data } = await axiosInstance.request(error.config);;
          return data;
        } catch(refreshError) {
          return rejectWithValue(refreshError.message);
        }
      } else {
        return rejectWithValue(error.message);
      }
    }
  }
);

Чтобы избежать повсеместной реализации такого поведения повторных попыток, вы можете попробовать использовать перехватчик ответа для абстрагирования и обработки логики повторных попыток.

Пример:

axiosInstance.js

import axios from 'axios';

const axiosInstance = axios.create({
  baseURL: 'http://localhost:8082/api',
  withCredentials: true,
});

export default axiosInstance;

userSlice.js

export const logoutUser = createAsyncThunk(
  "users/logoutUser",
  async (_, { rejectWithValue }) => {
    try {
      const { data } = await axiosInstance.post("/users/logout");
      return data;
    } catch(error) {
      return rejectWithValue(error.message);
    }
  }
);

main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux';

import App from './App.jsx'
import axiosInstance from './utils/axiosInstance';
import { store } from './app/store';
import { refreshUserToken } from './features/userSlice';

// Setup response interceptors for auth retry/token refresh
axiosInstance.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response.status === 401) {
      // Attempt token refresh, unwrap result
      await store.dispatch(refreshUserToken()).unwrap();

      // Retry original request
      return axiosInstance.request(error.config);
    }
    return Promise.reject(error);
  }
);

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <BrowserRouter>
      <Provider store = {store}>
        <App />
      </Provider>
    </BrowserRouter>
  </React.StrictMode>,
);