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

Как работать с асинхронным кодом в Redux?

2.0 Middle🔥 241 комментариев
#State Management#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Асинхронный код в Redux

Redux по умолчанию не поддерживает асинхронные операции, поэтому нужно использовать middleware или современные инструменты. Рассмотрим основные подходы.

1. Redux Thunk - классический подход

Thunk - это функция, которая возвращает другую функцию для отложенного выполнения:

import { createSlice, configureStore } from "@reduxjs/toolkit";
import thunk from "redux-thunk";

const userSlice = createSlice({
  name: "user",
  initialState: { data: null, loading: false, error: null },
  reducers: {
    startLoading: (state) => { state.loading = true; },
    setUser: (state, action) => { state.data = action.payload; state.loading = false; },
    setError: (state, action) => { state.error = action.payload; state.loading = false; }
  }
});

// Thunk action
const fetchUser = (userId) => async (dispatch) => {
  dispatch(startLoading());
  try {
    const response = await fetch(`/api/users/${userId}`);
    const data = await response.json();
    dispatch(setUser(data));
  } catch (error) {
    dispatch(setError(error.message));
  }
};

const store = configureStore({
  reducer: { user: userSlice.reducer },
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(thunk)
});

2. Redux Thunk в компоненте

Использование thunk action в React компоненте:

function UserComponent() {
  const dispatch = useDispatch();
  const { data, loading, error } = useSelector((state) => state.user);

  useEffect(() => {
    dispatch(fetchUser(123));
  }, [dispatch]);

  if (loading) return <div>Загрузка...</div>;
  if (error) return <div>Ошибка: {error}</div>;

  return <div>{data?.name}</div>;
}

3. Redux Toolkit с createAsyncThunk

Современный и рекомендуемый подход (Redux Toolkit):

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

// Определение async thunk
const fetchUser = createAsyncThunk(
  "user/fetchUser",
  async (userId) => {
    const response = await fetch(`/api/users/${userId}`);
    return response.json();
  }
);

const userSlice = createSlice({
  name: "user",
  initialState: { data: null, loading: false, error: null },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUser.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.data = action.payload;
        state.loading = false;
      })
      .addCase(fetchUser.rejected, (state, action) => {
        state.error = action.error.message;
        state.loading = false;
      });
  }
});

export default userSlice.reducer;
export { fetchUser };

4. Redux Saga - продвинутый подход

Использует генераторы для управления асинхронностью:

import { put, call, takeEvery } from "redux-saga/effects";

// Saga функция (генератор)
function* fetchUserSaga(action) {
  try {
    const response = yield call(
      fetch,
      `/api/users/${action.payload}`
    );
    const data = yield call(() => response.json());
    yield put(setUser(data));
  } catch (error) {
    yield put(setError(error.message));
  }
}

// Корневая saga
function* rootSaga() {
  yield takeEvery("FETCH_USER", fetchUserSaga);
}

const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  rootReducer,
  applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(rootSaga);

5. Redux Observable - с RxJS

Для тех, кто работает с реактивным программированием:

import { ofType } from "redux-observable";
import { switchMap, map, catchError } from "rxjs/operators";

const fetchUserEpic = (action$) =>
  action$.pipe(
    ofType("FETCH_USER"),
    switchMap((action) =>
      fetch(`/api/users/${action.payload}`).then((r) => r.json()).pipe(
        map((data) => setUser(data)),
        catchError((error) => of(setError(error.message)))
      )
    )
  );

6. Обработка ошибок и повторы

Реализация retry logic при ошибке:

const fetchUserWithRetry = createAsyncThunk(
  "user/fetchUser",
  async (userId, { rejectWithValue }) => {
    let lastError;
    
    for (let attempt = 0; attempt < 3; attempt++) {
      try {
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return await response.json();
      } catch (error) {
        lastError = error;
        if (attempt < 2) {
          await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1)));
        }
      }
    }
    
    return rejectWithValue(lastError.message);
  }
);

7. Сравнение подходов

// Redux Thunk: простой и прямолинейный
const fetchData = () => async (dispatch) => {
  const data = await api.get("/data");
  dispatch(setData(data));
};

// Redux Saga: больше контроля и сложности
function* fetchDataSaga() {
  const data = yield call(api.get, "/data");
  yield put(setData(data));
}

// createAsyncThunk: современный и удобный
const fetchData = createAsyncThunk("data/fetch", () => api.get("/data"));

8. Лучшие практики

// 1. Используй loading states для каждого запроса
const userSlice = createSlice({
  initialState: {
    entities: {},
    loading: {},
    error: {}
  }
});

// 2. Нормализуй состояние
const state = {
  users: {
    1: { id: 1, name: "Alice" },
    2: { id: 2, name: "Bob" }
  }
};

// 3. Используй requestIdGenerator для отмены запросов
const fetchUser = createAsyncThunk(
  "user/fetch",
  async (userId, { signal }) => {
    return fetch(`/api/users/${userId}`, { signal });
  }
);

9. Рекомендация

Для новых проектов рекомендую:

  1. Redux Toolkit + createAsyncThunk - стандартный выбор
  2. Redux Saga - если нужна сложная логика с множественными побочными эффектами
  3. Redux Thunk - для простых проектов
  4. RTK Query - если нужно управлять кешированием данных с сервера

Мой опыт: createAsyncThunk из Redux Toolkit - лучший выбор для большинства приложений. Он предоставляет удобный API и автоматически генерирует pending/fulfilled/rejected действия.