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

Асинхронные запросы (Thunk) 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, представляет из себя мидлвару, которая добавляется в 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
}

Примерно тоже самое можно сделать и без redux-thunk просто написав асинхронную функцию, которой мы на вход передадим dispatch(). Разница проявляется в более продвинутых вариантах использования. Например, когда нам нужно работать с состоянием или глобальными объектами, такими как вебсокет соединение. В этом случае придется использовать 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));
};

Несмотря на получаемые удобства, thunks сами по себе не уменьшают количество кода, и та же обработка ошибок составит большую часть кода. Здесь нам на помощь приходит createAsyncThunk():

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

// Создаем Thunk
export const fetchUserById = createAsyncThunk(
  'users/fetchUserById', // отображается в 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: {}, loading: 'idle', error: null }
  initialState: usersAdapter.getInitialState({ loading: 'idle', error: null }),
  reducers: {
    // любые редьюсеры, которые нам нужны
  },
  extraReducers: (builder) => {
    builder
      // Вызывается прямо перед выполнением запроса
      .addCase(fetchUserById.pending, (state) => {
        state.loading = 'loading';
        state.error = null;
      })
      // Вызывается в том случае если запрос успешно выполнился
      .addCase(fetchUserById.fulfilled, (state, action) => {
        // Добавляем пользователя
        usersAdapter.addOne(state, action.payload);
        state.loading = 'idle';
        state.error = null;
      })
      // Вызывается в случае ошибки
      .addCase(fetchUserById.rejected, (state, action) => {
        state.loading = '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 в тот момент, когда промис переходит в одно из этих состояний. Нам не обязательно реагировать на них все, мы сами выбираем, что нам важно в приложении.


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

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

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

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

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

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.

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

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

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

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

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

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

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

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

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Фронтенд-разработчик
Профессия
Разработка фронтенд-компонентов веб-приложений
1 июня 10 месяцев

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

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

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

Даю согласие на обработку персональных данных, соглашаюсь с «Политикой конфиденциальности» и «Условиями оказания услуг»