Эта статья предполагает рабочее понимание Javascript и Redux

Мы хотели бы поделиться тем, как мы соединяем наше приложение React и Redux с нашим API служб. Это было интересное путешествие, и мы довольны тем, что у нас есть.

Прежде всего, вы должны понимать, что мы используем аутентификацию с JWT. Мы отправляем токен в заголовке Authorization в каждом запросе.

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

Первый подход

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

Идея состоит в том, чтобы отправить действие с объектом, аналогичным объекту конфигурации, используемому в $.ajax. Затем этот объект перехватывается промежуточным программным обеспечением и отправляет запрос.

Вот как может выглядеть Action Creator:

const getFiles = () => ({
  type: API,
  path: '/files',
  onSuccess: addFiles,
});

Где addFiles - другой создатель действий, который будет отправлен вместе с ответом на вызов /files.

const addFiles = (files) => ({
  type: ADD_FILES,
  payload: files,
});

Промежуточное ПО отвечает за чтение токена пользователя из состояния Redux, создание с ним заголовков и выполнение запроса с использованием свойства path в действии.

Это простой пример того, как может выглядеть промежуточное ПО:

const middleware = ({ dispatch, getState }) => (next) => (action) => {
  if (action.type !== API) {
    return next(action);
  }
const token = getState().user.token;
  const headers = new Headers({
    Authorization: `Bearer ${token}`,
  });
  // see how we use `action.path`. Which in our example is `/files`
  const url = `https://www.someurl.com/api${action.path}`;
  return fetch(url, { headers })
    .then((res) => res.json())
    // `action.onSuccess` was `addFiles` which is an Action Creator
    .then((data) => dispatch(action.onSuccess(data)));
}

Проблемы с первым подходом

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

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

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

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

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

Вернуться к основам

Мы поняли, что ответ всегда был у нас перед носом. Мы использовали redux-thunk. Это был наш ответ. Мы могли бы использовать функцию async для преобразования опроса в синхронный код, используя цикл while и await.

const getFiles = () => async ({ dispatch, getState }) => {
  const token = getState().user.token;
  const headers = new Headers({
    Authorization: `Bearer ${token}`,
  });
  const url = 'https://www.someurl.com/api/files';
  const response = await fetch(url, { headers });
  const files = await res.json();
  const finalAction = addFiles(files);
  return dispatch(finalAction);
}

Помните, что в каждом запросе нам также нужно добавлять токен в заголовок. Делать это в каждом действии Thunk - не очень изящное решение. Ни целого URL.

Решение: Сервисный модуль

Мы поняли, что нам нужен модуль для взаимодействия с API служб.

Нам нужен модуль, который можно было бы использовать следующим образом:

const getFiles = () => async ({ dispatch }) => {
  const response = await services.getFiles();
  const files = await res.json();
  const finalAction = addFiles(files);
  return dispatch(finalAction);
};

Это действие просто и легко читается. Но что еще более важно, эти действия Thunk позволяют нам легко создавать сложные потоки. Например, опрос может быть примерно таким:

const uploadFile = (file) => async ({ dispatch }) => {
  const response = await services.getUploadUrl(file);
  const { newFile, uploadUrl } = await response.json():
  // we have a helper that upload a file to a URL
  await uploadFile(file, uploadUrl);
// the polling starts
  let retries = 0;
  let exists = false;
  while (!exists && retries <= MAX_RETRIES) {
    exists = await services.fileExists(newFile.id);
    retries += 1;
  }
if (!exists) {
    return dispatch(errorNotification('file did not upload successfully, please try again'))
  }
  return dispatch(addFile(newFile));
};

Действие uploadFile так же легко читается, как и предыдущее действие getFiles, даже несмотря на то, что поток более сложен.

Однако нам все еще нужно выяснить, где читать токен из состояния Redux.

Внедрение зависимости

Мы в браузере. Это означает, что наш services модуль будет использовать fetch для выполнения запросов.

const services = {
  getFiles: () => {
    const url = 'https://www.someurl.com/api/files';
    return fetch(url);
  },
};

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

Давайте создадим нашу собственную версию fetch.

const myFetch = (url, configuration) => {
  return fetch(url, configuration)
};

Давайте воспользуемся фабричной функцией для создания нашего myFetch.

const myFetchFactory = () => (url, configuration) => {
  return fetch(url, configuration)
};
const myFetch = myFetchFactory();

Почему мы это делаем? Хороший вопрос. Продолжай читать :)

Мы можем передать Redux Store на завод.

import appStore from 'store';
const myFetchFactory = (store) => (url, configuration) => {
  return fetch(url, configuration)
};
const myFetch = myFetchFactory(appStore);

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

const myFetchFactory = (store) => (url, configuration) => {
  const token = store.getState().user.token;
  const headers = new Headers({
    Authorization: `Bearer ${token}`,
  });
  const newConfiguration = {
    ...configuration,
    headers,
  };
  return fetch(url, newConfiguration);
};
const myFetch = myFetchFactory(appStore);
// Export `myFetch`
export default myFetch;

Наконец, мы добавляем наш myFetch, который будет использоваться в наших services модулях, вместо стандартного fetch.

// Import our depdendency
import myFetch from './myFetch';
const servicesFactory = (myFetch) => ({
  getFiles: () => {
    const url = 'https://www.someurl.com/api/files';
    return myFetch(url);
  },
});
const services = servicesFactory(myFetch);

Заключение

При наличии сложных асинхронных потоков используйте async и await для лучшей читаемости.

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

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

Изначально опубликовано в Блоге Onedot