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 [JS]](https://i.imgur.com/WsjO6zJb.png)


Вместо повторной отправки действия 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>,
);
@Abinash Ах, вероятно, цикл зависимостей между файлом, создающим хранилище, импортирующим редуктор срезов, хранилищем импорта файла axiosInstance и файлом среза, импортирующим axiosInstance. Вы можете создать отдельный файл вне этих трех, который должен иметь возможность безопасно импортировать как хранилище, так и axiosInstance, и создавать экземпляры перехватчиков, которые только что импортированы где-то в приложении, желательно рядом с корнем, или просто выполнить эту логику в корневом контейнере, где вы скорее всего, визуализируйте Redux Provider, куда вы уже импортируете магазин. Имеет ли это смысл?
Я сохранил store, userSlice, axiosInstance в разных файлах. ниже ссылка на демо: stackblitz.com/edit/…
@Abinash Верно, с этими тремя файлами ожидается именно это. Теперь попробуйте переместить импорт действий store и refreshUserToken, а также axiosInstance.interceptors.response.use(..... из файла axiosInstance в другое место, чтобы исключить цикл зависимостей. Я переместил их в ваш файл main.jsx, и после этого страница загрузилась и отобразилась. Есть ли способ проверить этот поток в вашей песочнице?
Спасибо за помощь, я переместил "axiosInstance.interceptors.response.use" внутрь файла main.jsx и реализовал его непосредственно в файле, а перед этим импортирую axiosInstance и сохраняю из другого файла. Теперь все работает. Перемещение «axiosInstance.interceptors» в новый файл (не main.jsx) и импорт туда axiosInstance с хранилищем также не работает, в этом случае перехватчик не выполняется.
@Abinash Если вы сделаете это в отдельном файле, скажем axiosInterceptors.js, то вам просто нужно будет импортировать этот файл, чтобы его код выполнился, т. е. import "./axiosInterceptors";. Имеет ли это смысл?
Я не импортировал файл при предыдущей попытке, он работает после импорта файла. Спасибо вам за помощь.
Я пробовал использовать перехватчики, как вы упомянули выше, но как только я импортирую хранилище, я сразу получаю сообщение об ошибке: ReferenceError: Невозможно получить доступ к userReducer перед инициализацией.