Redux - отличный инструмент, решающий одну из основных проблем UI-фреймворков: управление состоянием.

Управление состоянием на стороне клиента может быстро превратиться в кошмар, а однонаправленный поток данных, который обеспечивает Redux, позволяет легко понять, как события изменяют состояние вашего приложения.

Большой!

К сожалению, управление состоянием - лишь одна из многих проблем, с которыми вам приходится иметь дело при создании надежных приложений. А как насчет обработки побочных эффектов (например, сетевых запросов, наиболее распространенных)?

Redux сам по себе не предлагает готового решения. К счастью, у сообщества есть большое количество библиотек для решения этой проблемы.

Итак, что из этого подходит для вашего проекта?

Дело в том, что каждое из этих решений создавалось с учетом разных подходов, сценариев использования и ментальных моделей.

У всех есть свои плюсы и минусы.

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

Поддельный Medium, а?

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

Обратите внимание, что средний хлопок активен. Вот как я создал клон среднего хлопка на случай, если это вас заинтересует.

Даже для этого простого приложения вам нужно получать данные с сервера. Полезные данные JSON, необходимые для отображения требуемого представления, могут выглядеть следующим образом:

{
  "numberOfRecommends": 1900,
  "title": "My First Fake Medium Post",
  "subtitle": "and why it makes no intelligible sense",
  "paragraphs": [
    {
      "text": "This is supposed to be an intelligible post about something intelligible."
    },
    {
      "text": "Uh, sorry there’s nothing here."
    },
    {
      "text": "It’s just a fake post."
    },
    {
      "text": "Love it?"
    },
    {
      "text": "I bet you do!"
    }
  ]
}

Структура приложения действительно проста и состоит из двух основных компонентов: Article и Clap.

In components/Article.js :

Компонент article - это функциональный компонент без состояния, который принимает title, subtitle, и paragraphs реквизиты. Визуализированный компонент выглядит так:

const Article = ({ title, subtitle, paragraphs }) => {
  return (
    <StyledArticle>
      <h1>{title}</h1>
      <h4>{subtitle}</h4>
      {paragraphs.map(paragraph => <p>{paragraph.text}</p>)}
    </StyledArticle>
  );
};

Где StyledArticle - это обычный div элемент, стилизованный с помощью решения CSS-in-JS, styled-components.

Неважно, знакомы ли вы с каким-либо CSS в JS-решениях. StyledArticle можно заменить на div, оформленный с помощью старого доброго CSS.

Давай покончим с этим и не будем начинать спор 😂

In components/Clap.js :

Компонент среднего хлопка экспортируется в этот каталог. Код немного сложнее и выходит за рамки этой статьи. Тем не менее, вы можете прочитать о том, как я построил средний хлопок - это 5-минутное чтение.

При наличии обоих компонентов Clap и Article компонент App просто объединяет оба компонента, как показано на containers/App.js

class App extends Component {
  state = {};
  render() {
    return (
      <StyledApp>
        <aside>
          <Clap />
        </aside>
        <main>
          <Article />
        </main>
      </StyledApp>
    );
  }
}

Опять же, вы можете заменить StyledApp на обычный div и стилизовать его с помощью CSS.

Теперь перейдем к сути этой статьи.

Рассмотрение различных альтернативных решений для получения данных.

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

Самыми популярными вариантами, возможно, являются redux-thunk и redux-saga.

Готовый?

Redux Thunk и Redux-Promise

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

Из всех библиотек сообщества для управления побочными эффектами в Redux проще всего начать работу с теми, которые работают как Redux-thunk и Redux-promise.

Предпосылка проста.

Для redux-thunk вы пишете средство создания действия, которое не «создает» объект, а возвращает функцию. Этой функции передаются функции getState и dispatch от Redux.

Давайте посмотрим, как приложение-фальшивый носитель может использовать redux-thunk библиотеку.

Сначала установите библиотеку redux-thunk:

yarn add redux-thunk

Чтобы библиотека работала должным образом, ее необходимо использовать как промежуточное ПО.

In store/index.js

import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
const store = createStore(rootReducer, applyMiddleware(thunk));

Первая строка в блоке кода выше импортирует функции createStore и applyMiddleware из redux.

Line 2 импортирует thunk из redux-thunk.

Line 3 создает store, но с прикладным промежуточным программным обеспечением.

Теперь мы готовы сделать реальный сетевой запрос.

Я буду использовать axios библиотеку для выполнения сетевых запросов, но могу заменить ее любым http-клиентом по вашему выбору.

На самом деле, с redux-thunk инициировать сетевой запрос довольно просто. Вы создаете создателя действия, подобного этому (то есть создателя действия, который возвращает функцию):

export function fetchArticleDetails() {
  return function(dispatch) {
    return axios.get("https://api.myjson.com/bins/19dtxc")
      .then(({ data }) => {
      dispatch(setArticleDetails(data));
    });
  };
}

После установки компонента App вы отправляете этот создатель действия:

componentDidMount() {
    this.props.fetchArticleDetails();
 }

Вот и все. Обязательно отметьте полное различие кода, так как я выделяю здесь только ключевые строки:

При этом детали статьи были извлечены и отображены в приложении.

Что именно не так в таком подходе?

Если вы создаете очень маленькое приложение, redux-thunk решает проблему, и, возможно, с ним легче всего справиться.

Однако за простоту использования приходится платить. Рассмотрим три недостатка.

1. Каждый создатель действий будет иметь повторяющиеся функции для обработки ошибок и настройки заголовков.

Вот создатель действий, о котором мы писали ранее:

export function fetchArticleDetails() {
   return function(dispatch) {
     return axios.get("https://api.myjson.com/bins/19dtxc")
      .then(({ data }) => {
       dispatch(setArticleDetails(data));
     });
   };
}

В большинстве приложений вам нужно будет сделать несколько запросов и разными методами, GET, POST и т. Д.

Предположим, у вас есть другой создатель действий по имени recommendArticle. Теперь это может выглядеть так:

export function recommendArticle (id,  amountOfRecommends) {
  return  function (dispatch) {
   return axios.post("https://api.myjson.com/bins/19dtxc, {
       id,
      amountOfRecommends
})

О, а если вы хотите получить профиль пользователя?

export function fetchUserProfile() {
    return function(dispatch) {
      return axios.get("https://api.myjson.com/bins/19dtxc")
       .then(({ data }) => {
        dispatch(setUserProfile(data));
      });
    };
 }

Не нужно много времени, чтобы увидеть, что существует множество повторяющихся функций. А если вы хотите отлавливать ошибки, вы бы добавили catch блок для каждого создателя действий?

2. Чем больше создателей асинхронных действий, тем сложнее тестирование.

Асинхронный материал обычно сложнее тестировать. Это не невозможно или сложно протестировать, просто это значительно усложняет тестирование.

Сохранение создателей действий без сохранения состояния и упрощение их функций упрощает их отладку и тестирование.

Чем больше создателей действий вы включите в свое приложение, тем сложнее будет тестирование.

3. Еще сложнее изменить стратегию взаимодействия с сервером.

Что, если бы появился новый старший разработчик и решил, что команда должна перейти с axios на другой http-клиент, скажем, superagent. Теперь вам нужно будет изменить его для разных (нескольких) создателей действий.

Не так-то просто, правда?

Redux Saga и Redux-Observable

Они немного сложнее, чем redux-thunk или redux-promise.

redux-saga и redux-observable определенно лучше масштабируются, но они требуют обучения. Такие концепции, как саги и RxJS, необходимо изучить, и в зависимости от того, какой опыт имеют инженеры, работающие в команде, это может быть проблемой.

Итак, если redux-thunk и redux-promise слишком просты для вашего проекта, а redux-saga и redux-observable привнесут уровень сложности, который вы хотите абстрагироваться от своей команды, что делать?

Пользовательское промежуточное ПО!

Большинство решений, таких как redux-thunk, redux-promise и redux-saga, в любом случае используют промежуточное программное обеспечение под капотом. Почему ты не можешь создать свой?

Вы только что сказали: «Зачем изобретать велосипед?»

Это идеальное решение?

Хотя изобретение велосипеда звучит прямо как плохо, дайте ему шанс.

В любом случае многие компании уже создают индивидуальные решения, соответствующие их потребностям. Фактически, именно так началось множество проектов с открытым исходным кодом.

Итак, чего вы ожидаете от этого нестандартного решения?

  1. Централизованное решение, то есть в одном модуле.
  2. Может обрабатывать различные методы http, GET, POST, DELETE и PUT
  3. Может обрабатывать настройку пользовательских заголовков
  4. Поддерживает настраиваемую обработку ошибок, например для отправки в какую-либо внешнюю службу ведения журнала или для обработки ошибок авторизации.
  5. Разрешены обратные вызовы onSuccess и onFailure
  6. Поддерживает ярлыки для обработки состояний загрузки

Опять же, в зависимости от ваших конкретных потребностей, у вас может быть больший список.

А теперь позвольте мне подвести вас к достойной отправной точке. Тот, который вы можете адаптировать к своему конкретному варианту использования.

Промежуточное ПО redux всегда начинается так:

const apiMiddleware = ({dispatch}) => next => action => {
 next (action)
}

А вот и полноценный код для настраиваемого промежуточного программного обеспечения API. Поначалу это может показаться многословным, но я вскоре объясню каждую строчку.

Ну вот:

import axios from "axios";
import { API } from "../actions/types";
import { accessDenied, apiError, apiStart, apiEnd } from "../actions/api";


const apiMiddleware = ({ dispatch }) => next => action => {
  next(action);

  if (action.type !== API) return;

  const {
    url,
    method,
    data,
    accessToken,
    onSuccess,
    onFailure,
    label,
    headers
  } = action.payload;
  const dataOrParams = ["GET", "DELETE"].includes(method) ? 
  "params" : "data";
  // axios default configs
  axios.defaults.baseURL = process.env.REACT_APP_BASE_URL || "";
  axios.defaults.headers.common["Content-Type"]="application/json";
  axios.defaults.headers.common["Authorization"] = `Bearer${token}`;


  if (label) {
    dispatch(apiStart(label));
  }

  axios
    .request({
      url,
      method,
      headers,
      [dataOrParams]: data
    })
    .then(({ data }) => {
      dispatch(onSuccess(data));
    })
    .catch(error => {
      dispatch(apiError(error));
      dispatch(onFailure(error));

      if (error.response && error.response.status === 403) {
        dispatch(accessDenied(window.location.pathname));
      }
    })
   .finally(() => {
      if (label) {
        dispatch(apiEnd(label));
      }
   });
};

export default apiMiddleware;

Имея всего 100 строк кода, которые вы можете взять с GitHub, у вас есть индивидуальное решение с потоком, о котором легко рассуждать.

Я обещал объяснить каждую строчку, поэтому сначала рассмотрим, как работает промежуточное ПО:

Во-первых, вы сделаете несколько важных операций импорта, и очень скоро увидите, как они используются.

  1. Настройте промежуточное ПО

Это типичная установка, необходимая для промежуточного программного обеспечения redux. т.е.

const apiMiddleware = ({ dispatch }) => next => action => {
}

2. Отклоните нерелевантные типы действий

if (action.type !== API) return;

Вышеупомянутое условие важно для предотвращения запуска сетевого запроса любым действием, кроме действий типа API.

3. Извлеките важные переменные из полезной нагрузки действия

const {
    url,
    method,
    data,
    onSuccess,
    onFailure,
    label,
  } = action.payload;

Чтобы сделать успешный запрос, необходимо извлечь из полезной нагрузки действия следующее.

url представляет endpoint, который должен быть выполнен, method относится к HTTP-методу запроса, data относится к любым данным, которые должны быть отправлены на сервер, или параметру запроса, в случае запроса GET или DELETE, onSuccess и onFailure представляют любых создателей действий вы хотите отправить после успешного или неудачного запроса, а label относится к строковому представлению запроса.

Вскоре вы увидите их использование на практическом примере.

4. Обработка любого метода HTTP

const dataOrParams = ["GET", "DELETE"].includes(method) ? "params" : "data";

Поскольку в этом решении используется axios, и я думаю, что большинство клиентов HTTP в любом случае работают так, методы GET и DELETE используют params, в то время как другие методы могут потребовать отправки некоторого data на сервер.

Таким образом, переменная dataOrParams будет содержать любое из значений params или data в зависимости от метода запроса.

Если у вас есть опыт разработки в Интернете, это не должно быть странно.

5. Обработка глобальных переменных

  // axios default configs
  axios.defaults.baseURL = process.env.REACT_APP_BASE_URL || "";
  axios.defaults.headers.common["Content-Type"]="application/json";
  axios.defaults.headers.common["Authorization"] = `Bearer${token}`;

У большинства приличных приложений будет некоторый уровень авторизации, baseUrl и несколько заголовков по умолчанию. Технически, каждый клиент api, скорее всего, будет иметь некоторые значения по умолчанию для каждого запроса.

Это делается путем установки некоторых свойств объекта axios. Я не сомневаюсь, что то же самое можно сделать для любого клиента по вашему выбору.

6. Обработка состояний загрузки

if (label) {
    dispatch(apiStart(label));
}

Метка - это просто строка для идентификации определенного действия сетевого запроса. Прямо как тип действия.

Если label существует, промежуточное ПО отправит создателя действия apiStart.

Вот как выглядит создатель apiStart action:

export const apiStart = label => ({
  type: API_START,
  payload: label
});

Тип действия - API_START.

Теперь в ваших редукторах вы можете обрабатывать этот тип действия, чтобы знать, когда начинается запрос. Вскоре я покажу пример.

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

Опять же, вскоре я покажу пример.

7. Выполните фактический сетевой запрос, обработайте ошибки и вызовите обратные вызовы

axios
    .request({
      url: `${BASE_URL}${url}`,
      method,
      headers,
      [dataOrParams]: data
    })
    .then(({ data }) => {
      dispatch(onSuccess(data));
    })
    .catch(error => {
      dispatch(apiError(error));
      dispatch(onFailure(error));

      if (error.response && error.response.status === 403) {
        dispatch(accessDenied(window.location.pathname));
      }
    })
    .finally(() => {
      if (label) {
        dispatch(apiEnd(label));
      }
   });

Это не так сложно, как кажется.

axios.request отвечает за выполнение сетевого запроса с переданной конфигурацией объекта. Это переменные, которые вы извлекли из полезной нагрузки действия ранее.

После успешного запроса, как показано в блоке then, отправьте создателя действия apiEnd.

Это выглядит так:

export const apiEnd = label => ({
  type: API_END,
  payload: label
});

В вашем редукторе вы можете прослушивать это и отключать любые состояния загрузки по завершении запроса.

После этого отправьте обратный вызов onSuccess.

Обратный вызов onSuccess возвращает любое действие, которое вы хотите выполнить после успешного сетевого запроса. Почти всегда есть случай, когда после успешного сетевого запроса отправляется действие, например, чтобы сохранить полученные данные в хранилище redux.

Если возникает ошибка, как указано в блоке catch, также запускается apiEnd создатель действия, отправляя создатель действия apiError с ошибкой, в которой произошел сбой:

export const apiError = error => ({
  type: API_ERROR,
  error
});

У вас может быть другое промежуточное программное обеспечение, которое прослушивает этот тип действия и проверяет, что ошибка попадает в вашу внешнюю службу ведения журнала.

Вы также отправляете onFailure обратный вызов. На всякий случай вам нужно показать пользователю визуальную обратную связь. Это также работает для всплывающих уведомлений.

Наконец, я показал пример обработки ошибки аутентификации:

if (error.response && error.response.status === 403) {
     dispatch(accessDenied(window.location.pathname));
  }
});

В этом примере я отправляю создателя действия accessDenied, который принимает местоположение, в котором находился пользователь.

Затем я могу обработать это accessDenied действие в другом промежуточном программном обеспечении.

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

Вот и все!

Пользовательское промежуточное ПО в действии

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

import apiMiddleware from "../middleware/api";
const store = createStore(rootReducer, applyMiddleware(apiMiddleware));

А затем отредактируйте действие fetchArticleDetails, чтобы вернуть простой объект.

export function fetchArticleDetails() {
  return {
    type: API,
    payload: {
      url: "https://api.myjson.com/bins/19dtxc",
      method: "GET",
      data: null,
      onSuccess: setArticleDetails,
      onFailure: () => {
        console.log("Error occured loading articles");
      },
      label: FETCH_ARTICLE_DETAILS
    }
  };
}

function setArticleDetails(data) {
  return {
    type: SET_ARTICLE_DETAILS,
    payload: data
  };
}

Обратите внимание, как полезная нагрузка из fetchArticleDetails содержит всю необходимую информацию, требуемую промежуточным программным обеспечением.

Но есть небольшая проблема.

Как только вы выйдете за рамки одного создателя действия, каждый раз писать объект полезной нагрузки становится затруднительно. Особенно, если некоторые значения равны null или имеют значения по умолчанию.

Для простоты вы можете абстрагировать создание объекта действия новому создателю действия под названием apiAction

function apiAction({
  url = "",
  method = "GET",
  data = null,
  onSuccess = () => {},
  onFailure = () => {},
  label = ""
}) {
  return {
    type: API,
    payload: {
      url,
      method,
      data,
      onSuccess,
      onFailure,
      label
    }
  };
}

Используя параметры ES6 по умолчанию, обратите внимание, что apiAction уже имеет некоторые разумные значения по умолчанию.

Теперь в fetchArticleDetails вы можете сделать это:

function fetchArticleDetails() {
  return apiAction({
    url: "https://api.myjson.com/bins/19dtxc",
    onSuccess: setArticleDetails,
    onFailure:() => {console.log("Error occured loading articles")},
    label: FETCH_ARTICLE_DETAILS
  });
}

Это могло быть даже проще с некоторыми ES6:

const fetchArticleDetails = () => apiAction({
   url: "https://api.myjson.com/bins/19dtxc",
   onSuccess: setArticleDetails,
   onFailure: () => {console.log("Error occured loading articles")},
   label: FETCH_ARTICLE_DETAILS
});

Намного проще!

И результат тот же, приложение рабочее!

Чтобы увидеть, как ярлыки могут быть полезны для состояний загрузки, я буду обрабатывать типы действий API_START и API_END в редукторе.

case API_START:
  if (action.payload === FETCH_ARTICLE_DETAILS) {
     return {
        ...state,
        isLoadingData: true
     };
}
case API_END:
   if (action.payload === FETCH_ARTICLE_DETAILS) {
      return {
        ...state,
        isLoadingData: false
      };
 }

Теперь я устанавливаю флаг isLoadingData в объекте состояния на основе обоих типов действий, API_START и API_END.

На основании этого я могу настроить состояние загрузки в компоненте App.

Вот результат:

Это сработало!

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

Как бы то ни было, я использовал это как отправную точку для довольно крупных проектов, не сожалея о своем решении.

Заключение

Я определенно рекомендую вам опробовать различные доступные варианты выполнения сетевых запросов в приложении redux, прежде чем совершать какие-либо действия.

К сожалению, после выбора стратегии для выросшего приложения становится трудно провести рефакторинг.

В конце концов, это ваша команда, ваше приложение, ваше время и, в конечном итоге, только вы можете сделать выбор для себя.

Не забудьте заглянуть в репозиторий кода на Github, и спасибо https://leanpub.com/redux-book, из которого эта статья была вдохновлена.

Увидимся!

Разъем: LogRocket, видеорегистратор для веб-приложений.

LogRocket - это интерфейсный инструмент для ведения журнала, который позволяет воспроизводить проблемы так, как если бы они произошли в вашем собственном браузере. Вместо того, чтобы угадывать, почему происходят ошибки, или запрашивать у пользователей снимки экрана и дампы журнала, LogRocket позволяет воспроизвести сеанс, чтобы быстро понять, что пошло не так. Он отлично работает с любым приложением, независимо от фреймворка, и имеет плагины для регистрации дополнительного контекста из Redux, Vuex и @ ngrx / store.

Помимо регистрации действий и состояния Redux, LogRocket записывает журналы консоли, ошибки JavaScript, трассировки стека, сетевые запросы / ответы с заголовками и телами, метаданные браузера и пользовательские журналы. Он также использует DOM для записи HTML и CSS на странице, воссоздавая видео с идеальным пикселем даже для самых сложных одностраничных приложений.

Попробуйте бесплатно.