Как обрабатывать асинхронные действия в Redux?react-33

Обработка асинхронных операций в Redux требует специальных подходов, так как редьюсеры должны оставаться чистыми функциями. Рассмотрим основные методы и лучшие практики.

1. Redux Thunk

Популярное решение для простых асинхронных операций

Установка:

npm install redux-thunk

Настройка:

import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';

const store = createStore(rootReducer, applyMiddleware(thunk));

Пример запроса данных:

const fetchProducts = () => {
  return async (dispatch) => {
    dispatch({ type: 'PRODUCTS_REQUEST' });

    try {
      const response = await fetch('/api/products');
      const data = await response.json();
      dispatch({ type: 'PRODUCTS_SUCCESS', payload: data });
    } catch (error) {
      dispatch({ type: 'PRODUCTS_FAILURE', error: error.message });
    }
  };
};

// Использование:
dispatch(fetchProducts());

2. Redux Toolkit

Современный рекомендуемый подход

Пример с createAsyncThunk:

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

export const fetchUserData = createAsyncThunk(
  'users/fetchById',
  async (userId, { rejectWithValue }) => {
    try {
      const response = await fetch(`/api/users/${userId}`);
      return await response.json();
    } catch (err) {
      return rejectWithValue(err.response.data);
    }
  }
);

const usersSlice = createSlice({
  name: 'users',
  initialState: { data: null, status: 'idle', error: null },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserData.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchUserData.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.data = action.payload;
      })
      .addCase(fetchUserData.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.payload;
      });
  }
});

3. Redux Saga

Использует генераторы для управления side-эффектами

Установка:

npm install redux-saga

Пример:

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

function* fetchUser(action) {
  try {
    const user = yield call(fetch, `/api/users/${action.payload}`);
    yield put({ type: 'USER_FETCH_SUCCEEDED', payload: user });
  } catch (e) {
    yield put({ type: 'USER_FETCH_FAILED', error: e.message });
  }
}

function* mySaga() {
  yield takeEvery('USER_FETCH_REQUESTED', fetchUser);
}

// Подключение saga middleware
import createSagaMiddleware from 'redux-saga';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(mySaga);

4. RTK Query

Встроенный data-fetching инструмент в Redux Toolkit

Пример:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: (builder) => ({
    getProducts: builder.query({
      query: () => '/products',
    }),
  }),
});

// Использование в компоненте
const { data, error, isLoading } = useGetProductsQuery();

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

Метод Сложность Использование
Redux Thunk Низкая Простые асинхронные операции
createAsyncThunk Средняя Стандартные API запросы
Redux Saga Высокая Сложные workflows, отмена запросов
RTK Query Низкая Data fetching, кэширование

Паттерны обработки состояния

  1. Три состояния запроса:

    • pending (начало)
    • fulfilled (успех)
    • rejected (ошибка)
  2. Нормализация данных:

    {
      ids: [1, 2, 3],
      entities: {
        1: {id: 1, name: 'Product 1'},
        2: {id: 2, name: 'Product 2'}
      }
    }
    
  3. Оптимистичные обновления:

    // Redux Toolkit позволяет мутировать состояние в редьюсерах
    addCase(updateProduct.fulfilled, (state, action) => {
      const { id, ...changes } = action.payload;
      state.entities[id] = { ...state.entities[id], ...changes };
    }
    

Резюмируем

  1. Для простых случаев:

    • Используйте createAsyncThunk из Redux Toolkit
    • Или базовый redux-thunk
  2. Для сложных сценариев:

    • Рассмотрите redux-saga для сложных асинхронных workflows
    • Используйте RTK Query для data fetching
  3. Рекомендации:

    • Всегда обрабатывайте все состояния запроса (loading/error/success)
    • Нормализуйте данные для сложных структур
    • Для новых проектов начинайте с Redux Toolkit
    • Используйте инструменты разработчика для отладки

Пример обработки загрузки в компоненте:

function ProductsList() {
  const { data: products, isLoading, error } = useSelector(state => state.products);

  if (isLoading) return <Spinner />;
  if (error) return <Error message={error} />;

  return products.map(product => <ProductItem key={product.id} {...product} />);
}