← Назад к вопросам
Как сделать асинхронный запрос Redux?
2.3 Middle🔥 161 комментариев
#State Management
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Асинхронные запросы в Redux
Redux изначально синхронен, поэтому для работы с асинхронными операциями (API запросы) нужны middleware. Существует несколько подходов.
Способ 1: Redux Thunk (самый популярный)
Thunk - это функция, которая возвращает функцию вместо значения. Это позволяет отложить выполнение.
Установка
npm install redux-thunk
Настройка store
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./reducers";
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
export default store;
Создание async action
// actions/userActions.js
export const fetchUsers = () => {
// Thunk возвращает функцию
return async (dispatch, getState) => {
// dispatch - функция для отправки action
// getState - функция для получения текущего состояния
// 1. Диспатчим action загрузки
dispatch({ type: "FETCH_USERS_REQUEST" });
try {
// 2. Делаем асинхронный запрос
const response = await fetch("/api/users");
const data = await response.json();
// 3. Диспатчим success action
dispatch({
type: "FETCH_USERS_SUCCESS",
payload: data
});
} catch (error) {
// 4. Диспатчим error action
dispatch({
type: "FETCH_USERS_ERROR",
payload: error.message
});
}
};
};
Reducer
// reducers/userReducer.js
const initialState = {
users: [],
loading: false,
error: null
};
function userReducer(state = initialState, action) {
switch (action.type) {
case "FETCH_USERS_REQUEST":
return {
...state,
loading: true,
error: null
};
case "FETCH_USERS_SUCCESS":
return {
...state,
users: action.payload,
loading: false
};
case "FETCH_USERS_ERROR":
return {
...state,
loading: false,
error: action.payload
};
default:
return state;
}
}
export default userReducer;
Использование в компоненте
import { useDispatch, useSelector } from "react-redux";
import { fetchUsers } from "./actions/userActions";
function UserList() {
const dispatch = useDispatch();
const { users, loading, error } = useSelector(state => state.users);
React.useEffect(() => {
// Диспатчим async action
dispatch(fetchUsers());
}, [dispatch]);
if (loading) return <div>Загрузка...</div>;
if (error) return <div>Ошибка: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Способ 2: Redux Thunk с типизацией (TypeScript)
Для лучшей типизации используй typed thunk:
import { createSlice, configureStore } from "@reduxjs/toolkit";
import { ThunkAction, Action } from "@reduxjs/toolkit";
// Types
interface User {
id: string;
name: string;
}
interface UserState {
users: User[];
loading: boolean;
error: string | null;
}
// Slice
const userSlice = createSlice({
name: "users",
initialState: {
users: [],
loading: false,
error: null
} as UserState,
reducers: {
fetchUsersRequest: (state) => {
state.loading = true;
state.error = null;
},
fetchUsersSuccess: (state, action) => {
state.users = action.payload;
state.loading = false;
},
fetchUsersError: (state, action) => {
state.loading = false;
state.error = action.payload;
}
}
});
export const { fetchUsersRequest, fetchUsersSuccess, fetchUsersError } = userSlice.actions;
// Async thunk
export const fetchUsers = (): AppThunk => async (dispatch) => {
dispatch(fetchUsersRequest());
try {
const response = await fetch("/api/users");
const data = await response.json();
dispatch(fetchUsersSuccess(data));
} catch (error) {
dispatch(fetchUsersError(error instanceof Error ? error.message : "Unknown error"));
}
};
// Store
const store = configureStore({
reducer: {
users: userSlice.reducer
}
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
Способ 3: Redux Toolkit (рекомендуется)
Модерный и популярный подход с меньше boilerplate:
import { createSlice, createAsyncThunk, configureStore } from "@reduxjs/toolkit";
// createAsyncThunk генерирует pending, fulfilled, rejected actions
export const fetchUsers = createAsyncThunk(
"users/fetchUsers",
async (_, { rejectWithValue }) => {
try {
const response = await fetch("/api/users");
if (!response.ok) throw new Error("Network error");
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// Slice с extraReducers для async actions
const userSlice = createSlice({
name: "users",
initialState: {
users: [],
loading: "idle", // idle | pending | succeeded | failed
error: null
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.loading = "pending";
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = "succeeded";
state.users = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = "failed";
state.error = action.payload;
});
}
});
const store = configureStore({
reducer: {
users: userSlice.reducer
}
});
export default store;
Способ 4: Redux Saga (продвинутый)
Для сложной логики асинхронных операций:
import { call, put, takeEvery } from "redux-saga/effects";
import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
// Async операция
function fetchUsersAPI() {
return fetch("/api/users").then(res => res.json());
}
// Saga - generator функция
function* fetchUsersSaga() {
try {
yield put({ type: "FETCH_USERS_REQUEST" });
const users = yield call(fetchUsersAPI); // call - вызвать функцию
yield put({ type: "FETCH_USERS_SUCCESS", payload: users });
} catch (error) {
yield put({ type: "FETCH_USERS_ERROR", payload: error.message });
}
}
// Root saga - следить за экшенами
function* rootSaga() {
yield takeEvery("FETCH_USERS", fetchUsersSaga);
}
// Настройка store
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
export default store;
Сравнение подходов
const comparison = {
thunk: {
плюсы: [
"Простой и понятный",
"Минимум boilerplate",
"Встроен в redux"
],
минусы: [
"Сложная логика становится запутанной",
"Нет cancel/timeout",
"Testing немного сложнее"
],
использование: "95% приложений"
},
toolkit: {
плюсы: [
"Меньше boilerplate",
"createAsyncThunk для API",
"Встроен DevTools"
],
минусы: [
"Ещё одна зависимость",
"Learning curve"
],
использование: "Рекомендуется для новых проектов"
},
saga: {
плюсы: [
"Мощная и гибкая",
"Легко тестировать",
"Сложные flow"
],
минусы: [
"Steep learning curve",
"Generator functions",
"Больше кода"
],
использование: "Большие и сложные приложения"
}
};
Практический полный пример с Redux Toolkit
// slices/todosSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
export const fetchTodos = createAsyncThunk(
"todos/fetchTodos",
async (userId, { rejectWithValue }) => {
try {
const response = await fetch(`/api/todos?userId=${userId}`);
if (!response.ok) throw new Error("Failed to fetch");
return await response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const todosSlice = createSlice({
name: "todos",
initialState: {
items: [],
loading: false,
error: null
},
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.loading = true;
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
});
}
});
export default todosSlice.reducer;
// В компоненте
function TodoList({ userId }) {
const dispatch = useDispatch();
const { items, loading, error } = useSelector(state => state.todos);
useEffect(() => {
dispatch(fetchTodos(userId));
}, [userId, dispatch]);
if (loading) return <p>Загрузка...</p>;
if (error) return <p>Ошибка: {error}</p>;
return (
<ul>
{items.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
Ключевые моменты
- Redux Thunk - самый простой для async операций
- Redux Toolkit - modern standard для Redux
- Всегда обрабатывай три состояния: loading, success, error
- Используй createAsyncThunk для чистого кода
- Для testing: mockируй API, тестируй reducer отдельно
- Не забывай про cancel для unmounted компонентов
- Рассмотри zustand или MobX если Redux overkill