В отличие от синхронных запросов, которые выполняются здесь и сейчас, асинхронные растянуты во времени. Каждый асинхронный запрос можно представить конечным автоматом с тремя состояниями "something requested", "answer received" или "request failed". Почему это важно? Как минимум нам важно знать когда запрос был выполнен, чтобы оповестить пользователя и произвести необходимые изменения. Но также важно знать когда запрос начался.

Предположим, что пользователь заполнил форму создания новой задачи и нажал "создать", а потом быстро нажал "создать" ещё раз до того, как запрос успел выполниться. Такая ситуация нередко встречается и с обычными формами без JS. Она приводит к тому, что на сервере создаются две одинаковые сущности, либо выскакивают ошибки, связанные с валидацией. Правильное решение в подобной ситуации связано с необходимостью менять интерфейс так, чтобы повторная отправка стала невозможной. Как правило, всё сводится к блокированию кнопки отправки с крутящимся спинером. Соответственно, после окончания запроса кнопку необходимо разблокировать или, если того требует UI, вообще убрать форму. То же самое нужно делать не только в случае успеха, но и в случае провала, иначе может получиться ситуация, что пользовательский запрос провалился, а кнопка осталась заблокирована навсегда (до перезагрузки страницы).

С точки зрения нашего React-Redux приложения автомат будет состоять из состояния в контейнере и трёх событий:

  • TASK_UPDATE_REQUEST.
  • TASK_UPDATE_SUCCESS.
  • TASK_UPDATE_FAILURE.

Именование в стиле request, success и failure — рекомендация самого Redux. Желательно всегда придерживаться именно её в случаях когда состояния три. Большинство запросов укладываются именно в эту схему.

export const updateTaskRequest = createAction('TASK_UPDATE_REQUEST');
export const updateTaskSuccess = createAction('TASK_UPDATE_SUCCESS');
export const updateTaskFailure = createAction('TASK_UPDATE_FAILURE');

Действия, описанные выше, по сути, синхронны. А где тогда выполняется сам запрос?

Для этого вводится понятие асинхронные действия (async actions). И если в React для работы с асинхронными вызовами ничего дополнительно делать не нужно, то Redux из коробки это не умеет. Наиболее простой способ начать выполнять запросы на сервер — подключить библиотеку redux-thunk. Она представляет из себя мидлвар, который нужно не забыть подключить:

import thunk from 'redux-thunk';

const store = createStore(
  reducers,
  /* ... */,
  applyMiddleware(thunk),
);

На этом интеграция заканчивается. Следующим шагом создаются сами действия:

export const updateTask = (id, values) => async (dispatch) => {
  dispatch(updateTaskRequest());
  try {
    const response = await axios.patch(routes.taskUrl(id), { task: values });
    dispatch(updateTaskSuccess({ task: response.data }));
  } catch (e) {
    // Обязательно выводите ошибку, иначе вы не узнаете что пошло не так при отладке
    console.log(e);
    dispatch(updateTaskFailure());
  }
};

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

  1. Уведомляем Redux о начале внешнего запроса.
  2. Выполняем внешний запрос.
  3. Если запрос выполнился удачно, уведомляем Redux и передаём туда полученные данные (если есть).
  4. Если запрос выполнился неудачно, уведомляем Redux.

Этот паттерн встречается в реальных приложениях крайне часто.

Посмотрим, как в коде React вызывается обработчик, выполняющий наше действие:

class EditTaskForm extends React.Component {
  handleUpdateTask = (values) => {
    const { updateTask, task } = this.props;
    updateTask(task.id, values);
  }

  render() {
    const { taskCreatingState } = this.props;
    const disabled = taskCreatingState === 'requested';

    return (
      <form action="" className="form-inline" onSubmit={this.props.handleSubmit(this.handleUpdateTask)}>
        <div className="form-group mx-3">
          <Field name="text" required component="input" type="text" />
        </div>
        <button type="submit" disabled={disabled} className="btn btn-primary btn-sm">Update</button>
      </form>
    );
  }
}

export default reduxForm({
  form: 'editTask',
})(EditTaskForm);

Из кода выше видно, что действие вызывается как обычно.

Библиотека redux-thunk всего лишь один из многих способов работы с асинхронными действиями в Redux. Существуют и другие пакеты, дающие больший контроль и больший уровень автоматизации. Но, как правило, они сложнее в понимании.

Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →