Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Асинхронные запросы React: Redux Toolkit

Проблематика работы с асинхронными запросами

Одна из самых сложных задач в построении фронтенд-приложений – работа с внешними запросами. Трудности приходят с двух сторон.

С одной стороны, асинхронность сама по себе порождает неоднозначности, перестают работать стандартные механизмы. Redux не умеет работать в асинхронном режиме, поэтому вся обработка запросов происходит снаружи. В таком случае любая нетривиальная логика обработки асинхронных действий будет появляться внутри компонентов React:

const MyComponent = (props) => {
  const onClick = async (todoId) => {
    const response = await axios.get(`https://ru.hexlet.io/api/todos/${todoId}`);
    const todos = // Извлекаем и преобразуем данные из response
    dispatch(todosLoaded(todos));
  };

  // Рендер
};

С другой стороны, сеть — это вещь ненадежная. Запросы могут выполняться долго или не выполниться вообще, и все это нужно отслеживать для правильной реакции:

  • При долгих запросах — показывать спиннер
  • При обрыве запроса — выводить соответствующее предупреждение

Добавим к примеру выше обработку ошибок и отслеживание статуса загрузки:

const onClick = async (todoId) => {
  // Здесь мы работаем с конечным автоматом по обработке любого http-запроса
  try {
    // Начинаем процесс загрузки
    dispatch(todosLoadingStarted());
    const response = await axios.get(`https://ru.hexlet.io/api/todos/${todoId}`);
    const todos = // Извлекаем и преобразуем данные из response
    dispatch(todosLoaded(todos));
  } catch (e) {
    // Все еще сложнее — нужно отслеживать, что конкретно пошло не так
    dispatch(todosLoadingFailed(e.message));
  }
};

Как мы видим, даже для небольшого числа вызовов придется написать очень много похожего кода. В реальных приложениях количество вызовов может измеряться многими десятками и даже сотнями. Поэтому без готового решения тут не обойтись. Для автоматизации HTTP-запросов к нам на помощь приходят два механизма:

  • Мидлвара redux-thunk, которая уже включена в Redux Toolkit
  • Механизм createAsyncThunk()

Мидлвара redux-thunk

Мидлвара — это код, который встраивается в обработку. Можно представить ее в виде цепочки функций, где каждая функция принимает данные из предыдущего обработчика и передает данные в следующую функцию. Каждая такая функция и будет мидлварой. Самый простой пример — это вывод логов. Такая функция будет выводить в лог данные и передавать дальше, никак их не меняя:

const loggerMiddleware = (store) => (next) => (action) => {
  // Вывод информации о действии в консоль
  console.log('Dispatching action:', action);

  // Передача действия дальше по цепочке middleware
  return next(action);
};

Мидлвара redux-thunk добавляется в Redux и позволяет использовать асинхронный код внутри dispatch(). С ее помощью выносят логику выполнения запросов и обновления хранилища в отдельные функции (thunks). Вот пример такой функции:

// Обратите внимание на вложенную функцию, принимающую dispatch
// Эта функция будет храниться вне компонента — например, в слайсе
export const fetchTodoById = (todoId) => async (dispatch) => {
  const response = await axios.get(`https://ru.hexlet.io/todos/${todoId}`);
  // Здесь нужно выполнить необходимую нормализацию и обработать ошибки
  dispatch(todosLoaded(response.todos));
};

// Использование
const TodoComponent = ({ todoId }) => {
  const dispatch = useDispatch();

  const onFetchClicked = () => {
    // Передали асинхронную функцию
    dispatch(fetchTodoById(todoId));
  };

  // Где-то здесь используем onFetchClicked
};

Вообще thunk необязательно должен быть асинхронным. Thunk — это всего лишь функция, которая возвращает другую функцию и принимает dispatch и getState (при необходимости) в качестве параметров. Thunk может выполнять синхронные действия или комбинации синхронных и асинхронных операций, но в этом уроке нам это не так важно.

Код из примера выше можно реализовать и без redux-thunk, просто написав асинхронную функцию. Ей на вход мы передадим dispatch:

export const fetchTodoById = async (todoId, dispatch) => {
  try {
    const response = await axios.get(`https://ru.hexlet.io/todos/${todoId}`);
    // Выполнить необходимую нормализацию и обработать ошибки
    dispatch(todosLoaded(response.data.todos));
  } catch (error) {
    // Обработка ошибок
    dispatch(todosLoadingFailed(error.message));
  }
};

Разница проявляется в более продвинутых вариантах использования — например, когда мы работаем с состоянием или глобальными объектами. В этом случае не обойтись без redux-thunk:

// (dispatch, getState, extraArgument)
export const fetchTodoById = (todoId) => async (dispatch, getState, extraArgument) => {
  // Любые данные, переданные на этапе конфигурации мидлвары
  const { serviceApi } = extraArgument;
  const response = await serviceApi.getTodo(todoId);
  dispatch(todosLoaded(response.todos));
};

Основное отличие здесь — это возможность передачи дополнительных параметров в thunk-функцию через extraArgument, а также быстро получить текущее состояние хранилища с помощью функции getState.

Механизм createAsyncThunk()

Несмотря на удобства redux-thunk, сами по себе thunks не уменьшают количество кода. Та же обработка ошибок всё еще составляет большую его часть. Здесь на помощь приходит инструмент createAsyncThunk(), появившийся вместе с redux-toolkit:

import { createAsyncThunk, createSlice, createEntityAdapter } from '@reduxjs/toolkit';
// Чтобы не хардкодить URL-адреса, делаем модуль, в котором они создаются
import { getUserUrl } from './routes.js';

// Создаем thunk
export const fetchUserById = createAsyncThunk(
  'users/fetchUserById', // Id отображается в dev tools и должен быть уникальный у каждого thunk
  async (userId) => {
    // Здесь только логика запроса и возврата данных
    // Никакой обработки ошибок
    const response = await axios.get(getUserUrl(userId));
    return response.data;
  }
);

const usersAdapter = createEntityAdapter();

const usersSlice = createSlice({
  name: 'users',
  // Добавляем в состояние отслеживание процесса загрузки
  // { ids: [], entities: {}, loadingStatus: 'idle', error: null }
  initialState: usersAdapter.getInitialState({ loadingStatus: 'idle', error: null }),
  reducers: {
    // Любые редьюсеры, которые нам нужны
  },
  extraReducers: (builder) => {
    builder
      // Вызывается прямо перед выполнением запроса
      .addCase(fetchUserById.pending, (state) => {
        state.loadingStatus = 'loading';
        state.error = null;
      })
      // Вызывается, если запрос успешно выполнился
      .addCase(fetchUserById.fulfilled, (state, action) => {
        // Добавляем пользователя
        usersAdapter.addOne(state, action);
        state.loadingStatus = 'idle';
        state.error = null;
      })
      // Вызывается в случае ошибки
      .addCase(fetchUserById.rejected, (state, action) => {
        state.loadingStatus = 'failed';
        // https://redux-toolkit.js.org/api/createAsyncThunk#handling-thunk-errors
        state.error = action.error;
      });
  },
})

// Где-то в приложении
import { fetchUserById } from './slices/usersSlice.js';

// Внутри компонента
dispatch(fetchUserById(123));

Каждый thunk, созданный через createAsyncThunk(), содержит внутри себя три события:

  • pending
  • fulfilled
  • rejected

Они соответствуют состояниям промиса и вызываются в Redux Toolkit в тот момент, когда промис переходит в одно из этих состояний. Нам не обязательно реагировать на все. Мы сами выбираем, что нам важно в приложении.

Применение thunk

Как отмечалось ранее, Thunk выходит за рамки обработки асинхронных запросов. Этот механизм можно использовать в различных сценариях, где требуется вынос сложной логики или побочных эффектов из компонентов. Thunk также оказывается полезным для написания логики, зависящей от состояния в Redux. Ещё одним важным аспектом использования Thunk является возможность отправки нескольких действий в определенный момент или в течение заданного времени.

При этом Thunk дополняет, а не заменяет useEffect, который по-прежнему может использоваться для работы с побочными эффектами в компонентах. Если необходимо выделить какую-то логику из компонента, чтобы сделать его более универсальным для повторного использования, эту логику можно перенести в Thunk вместо использования useEffect.

На практике useEffect и createAsyncThunk часто используются вместе. Например, вызов dispatch(fetchData()) внутри useEffect с пустым массивом зависимостей позволяет подгрузить данные при инициализации компонента, обеспечивая эффективное взаимодействие между асинхронными операциями и жизненным циклом компонента.

Что дальше

В современной разработке вместе с React часто используется TypeScript. Вы можете познакомиться с ним в курсе Основы Typescript.


Дополнительные материалы

  1. Как работает Redux Thunk
  2. Документация createAsyncThunk
  3. Автоматное программирование

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 6 300 ₽ в месяц
Разработка фронтенд-компонентов для веб-приложений
10 месяцев
с нуля
Старт 21 марта
профессия
от 9 900 ₽ в месяц
Разработка фронтенд- и бэкенд-компонентов для веб-приложений
16 месяцев
с нуля
Старт 21 марта

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»