← Назад к вопросам

Как сделать асинхронный запрос 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