Статьи

Асинхронные операции в приложениях React Redux

Этот пост был первоначально размещен на Codebrahma .

JavaScript — это однопоточный язык программирования. То есть, когда у вас есть код что-то вроде этого …

асинхронная реакция, избыточность

… Вторая строка не выполняется, пока не завершится первая. В основном это не будет проблемой, поскольку миллионы вычислений выполняются клиентом или сервером за секунду. Мы замечаем эффекты только тогда, когда выполняем дорогостоящие вычисления (задача, выполнение которой занимает заметное время — сетевой запрос, для возврата которого требуется некоторое время).

Почему я показал только вызов API (сетевой запрос) здесь? А как насчет других асинхронных операций? Вызов API является очень простым и полезным примером для описания того, как справляться с асинхронной операцией. Существуют и другие операции, такие как setTimeout()

При структурировании нашего приложения мы должны учитывать, как асинхронное выполнение влияет на структурирование. Например, рассмотрим fetch() (Забудьте, если это AJAX-запрос. Просто представьте, что это поведение асинхронное или синхронное по своей природе.) Прошедшее время, пока запрос обрабатывается на сервере, не происходит в главном потоке. Таким образом, ваш JS-код будет продолжать выполняться, и как только запрос вернет ответ, он обновит поток.

Рассмотрим этот код:

 userId = fetch(userEndPoint); // Fetch userId from the userEndpoint
userDetails = fetch(userEndpoint, userId) // Fetch for this particular userId.

В этом случае, поскольку fetch()userIduserDetails Поэтому нам нужно структурировать его так, чтобы вторая строка выполнялась только тогда, когда первая возвращает ответ.

Большинство современных реализаций сетевых запросов являются асинхронными. Но это не всегда помогает, так как мы зависим от предыдущих данных ответа API для последующих вызовов API. Давайте посмотрим, как конкретно мы можем структурировать это в приложениях ReactJS / Redux.

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

  • ясность кода
  • масштабируемость
  • простота обработки ошибок.

Для каждого метода мы выполним эти два вызова API:

1. Выбор города из userDetails (Первый ответ API)

Давайте предположим, что конечной точкой является /details Это будет иметь город в ответе. Ответом будет объект:

 userDetails : {
  …
  city: 'city',};

2. В зависимости от города пользователя мы выберем все рестораны в городе.

Допустим, конечной точкой является /restuarants/:city Ответом будет массив:

 ['restaurant1', 'restaurant2',]

Помните, что мы можем выполнить второй запрос только тогда, когда мы закончим делать первый (так как это зависит от первого запроса). Давайте посмотрим на различные способы сделать это:

  • напрямую используя обещание или асинхронное ожидание с setState
  • используя Redux Thunk
  • используя Redux-Saga
  • используя Redux Observables.

В частности, я выбрал вышеупомянутые методы, потому что они наиболее широко используются для крупномасштабного проекта. Есть и другие методы, которые могут быть более специфичными для конкретных задач и которые не обладают всеми функциями, необходимыми для сложного приложения (например, redux-async, redux-обещание, redux-async-queue ).

обещания

Обещание — это объект, который может выдавать одно значение в будущем: либо разрешенное значение, либо причина, по которой оно не разрешено (например, произошла ошибка сети). Эрик Эллиот

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

 componentDidMount() {
  axios.get('/details') // Get user details
    .then(response => {
    const userCity = response.city;
    axios.get(`/restaurants/${userCity}`)
      .then(restaurantResponse => {
       this.setState({
         listOfRestaurants: restaurantResponse, // Sets the state
       })
    })
  })
}

Таким образом, когда состояние изменяется (из-за выборки), Компонент автоматически выполнит повторную визуализацию и загрузит список ресторанов.

Async/await Например, то же самое может быть достигнуто этим:

 async componentDidMount() {
  const restaurantResponse = await axios.get('/details') // Get user details
    .then(response => {
    const userCity = response.city;
    axios.get(`/restaurants/${userCity}`)
      .then(restaurantResponse => restaurantResponse
    });

    this.setState({
      restaurantResponse,
    });
}

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

Недостатки в методе

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

асинхронные государственные вопросы

  • Мы не хотим, чтобы поток, в котором выполняется JS, был заблокирован для сетевого запроса.
  • Все вышеперечисленные случаи сделают код очень сложным и сложным в обслуживании и тестировании.
  • Кроме того, масштабируемость будет большой проблемой, поскольку, если мы планируем изменить поток приложения, нам необходимо удалить все выборки из компонента.
  • Представьте, что вы делаете то же самое, если компонент находится в верхней части родительского дочернего дерева. Затем нам нужно изменить все зависимые от данных компоненты представления.
  • Также следует отметить, что вся бизнес-логика находится внутри компонента.

Как мы можем улучшить здесь?

1. Государственное управление
В этих случаях использование глобального магазина фактически решит половину наших проблем. Мы будем использовать Redux в качестве нашего глобального магазина.

2. Перемещение бизнес-логики в правильное место
Если мы думаем о переносе нашей бизнес-логики за пределы компонента, то где именно мы можем это сделать? В действиях? В редукторах? Через промежуточное ПО? Архитектура Redux такова, что она синхронна по своей природе. В тот момент, когда вы отправляете действие (объекты JS) и оно достигает хранилища, редуктор воздействует на него.

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

избыточная архитектура

Исходя из этого, мы можем получить представление о том, что если мы перемещаем всю логику извлечения перед редуктором — то есть действием или промежуточным программным обеспечением — тогда можно отправить правильное действие в нужное время.
Например, как только выборка начинается, мы можем dispatch({ type: 'FETCH_STARTED' })dispatch({ type: 'FETCH_SUCCESS' })

Хотите разработать приложение React JS?

Использование Redux Thunk

Redux Thunk — это промежуточное ПО для Redux. Это в основном позволяет нам возвращать functionobjects Это помогает, предоставляя dispatchgetState Мы эффективно используем отправку, отправляя необходимые действия в нужное время. Преимущества:

  • разрешить множественные отправки внутри функции
  • привязка бизнес-логики к выборке будет вне компонентов React и перенесена в действия.

В нашем случае мы можем переписать действие следующим образом:

 export const getRestaurants = () => {
  return (dispatch) => {
  dispatch(fetchStarted()); // fetchStarted() returns an action

  fetch('/details')
    .then((response) => {
      dispatch(fetchUserDetailsSuccess()); // fetchUserDetailsSuccess returns an action
      return response;
     })
    .then(details => details.city)
    .then(city => fetch('/restaurants/city'))
    .then((response) => {
      dispatch(fetchRestaurantsSuccess(response)) // fetchRestaurantsSuccess(response) returns an      action with the data
    })
    .catch(() => dispatch(fetchError())); // fetchError() returns an action with error object
  };
}

Как видите, теперь у нас есть хороший контроль над тем, когда dispatch Каждый вызов функции, например fetchStarted()fetchUserDetailsSuccess()fetchRestaurantsSuccess()fetchError() Теперь задача редукторов — обрабатывать каждое действие и обновлять представление. Я не обсуждал редуктор, поскольку отсюда все просто и реализация может быть разной.

Чтобы это работало, нам нужно соединить компонент React с Redux и связать действие с компонентом с помощью библиотеки Redux. Как только это будет сделано, мы можем просто вызвать this.props.getRestaurants()

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

Но все же, это немного сложно сделать определенные задачи, используя Redux Thunk. Например, нам нужно приостановить выборку между ними, или когда есть несколько таких вызовов, и разрешить только самые последние, или если какой-то другой API извлекает эти данные, и нам нужно отменить.

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

Использование Redux-Saga

Используя промежуточное программное обеспечение Redux-Saga, мы можем получить дополнительные преимущества, которые решают большинство вышеупомянутых функций. Redux-Saga был разработан на базе генераторов ES6.

Redux-Saga предоставляет API, который помогает достичь следующего:

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

Как работают саги?

Sagas использует комбинацию генераторов ES6 и API-интерфейсов async await для упрощения асинхронных операций. Он в основном работает в отдельном потоке, где мы можем выполнять несколько вызовов API. Мы можем использовать их API, чтобы сделать каждый вызов синхронным или асинхронным в зависимости от варианта использования. API предоставляет функциональные возможности, с помощью которых мы можем заставить поток ждать в одной строке, пока запрос не вернет ответ. Помимо этого, есть много других API, предоставляемых этой библиотекой, что делает запросы API очень простыми в обработке.

Рассмотрим наш предыдущий пример: если мы инициализируем сагу и настраиваем ее с Redux, как указано в их документации, мы можем сделать что-то вроде этого:

 import { takeEvery, call } from 'redux-saga/effects';
import request from 'axios';

function* fetchRestaurantSaga() {

  // Dispatches this action once started
  yield put({ type: 'FETCH_RESTAURANTS_INITIATED '});

  try {
    // config for fetching details API
    const detailsApiConfig = {
      method: 'get',
      url: '/details'
    };
    // Blocks the code at this line till it is executed
    const userDetails = yield call(request, config);

    // config for fetching details API
    const restaurantsApiConfig = (city) {
      method: 'get',
      url: `/restaurants/${city}`,
    };

    // Fetches all restuarants
    const restaurants = yield call(request, restaurantsApiConfig(userDetails.city));

    // On success dispatch the restaurants
    yield put({
      type: 'FETCH_RESTAURANTS_SUCCESS',
      payload: {
        restaurants
      },
    });

  } catch (e) {
    // On error dispatch the error message
    yield put({
      type: 'FETCH_RESTAURANTS_ERROR',
      payload: {
        errorMessage: e,
      }
    });
  }
}

export default function* fetchRestaurantSagaMonitor() {
  yield takeEvery('FETCH_RESTAURANTS', fetchInitial); // Takes every such request
}

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

  • когда начался первый запрос
  • когда первый запрос закончен
  • когда начался второй запрос

… и так далее.

Также вы можете увидеть красоту fetchRestaurantsSaga() В настоящее время мы использовали API вызовов для реализации блокировки вызовов. Sagas предоставляют другие API, такие как fork() Мы можем комбинировать как блокирующие, так и неблокирующие вызовы, чтобы поддерживать структуру, которая соответствует нашему приложению.

С точки зрения масштабируемости, использование саг выгодно:

  • Мы можем структурировать и группировать саги на основе конкретных задач. Мы можем вызвать одну сагу из другой, просто отправив действие.
  • Поскольку это промежуточное программное обеспечение, действия, которые мы пишем, будут обычными объектами JS, в отличие от thunks.
  • Поскольку мы перемещаем бизнес-логику в саги (которая является промежуточным программным обеспечением), если мы знаем, какова будет функциональность саги, то понимание ее части React будет намного проще.
  • Ошибки могут быть легко отслежены и отправлены в магазин с помощью шаблона try / catch.

Использование Redux-Observables

Как упомянуто в их документации под заголовком «Эпопея является основным примитивом наблюдаемого редукса »:

  1. Epic — это функция, которая принимает поток действий и возвращает поток действий. То есть Epic работает вместе с обычным каналом отправки Redux после того, как редукторы уже получили их.

  2. Действия всегда проходят через ваши редукторы, прежде чем эпики даже получат их. Эпос просто получает и выводит другой поток действий. Это похоже на Redux-Saga, в котором промежуточное программное обеспечение не использует ни одно из действий. Он просто слушает и выполняет некоторые дополнительные задачи.

Для нашей задачи мы можем просто написать это:

 const fetchUserDetails = action$ => (
  action$.ofType('FETCH_RESTAURANTS')
    .switchMap(() =>
      ajax.getJSON('/details')
        .map(response => response.userDetails.city)
        .switchMap(() =>
          ajax.getJSON(`/restaurants/city/`)
            .map(response => ({ type: 'FETCH_RESTAURANTS_SUCCESS', payload: response.restaurants })) // Dispatching after success
)
         .catch(error => Observable.of({ type: 'FETCH_USER_DETAILS_FAILURE', error }))
      )
    )
)

Сначала это может показаться немного запутанным. Но чем больше вы понимаете RxJS, тем легче создать Epic.

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

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

Мои предпочтения

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

Как вы выбираете между Redux-Saga и Redux-Observable?
Все сводится к обучающим генераторам или RxJS. Оба понятия разные, но одинаково хорошие. Я бы предложил попробовать оба, чтобы увидеть, какой из них подходит вам лучше всего.

Где вы храните свою бизнес-логику, имея дело с API?
Желательно перед редуктором, но не в компоненте. Наилучшим способом будет промежуточное программное обеспечение (использование саг или наблюдаемых).

Вы можете прочитать больше сообщений о разработке React на Codebrahma .