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

Конкретные темы, которые мы обсудим:

  • Один раз запустить сагу с take вместо takeLatest или takeEvery
  • Ожидание первого завершенного действия с race
  • Совместное использование сервисов в вашем приложении с помощью getContext

Давайте прыгать прямо в!

Один раз запустить сагу с take вместо takeLatest или takeEvery

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

Но что, если нас интересует только первое появление действия, например первое взаимодействие пользователя с формой? Как бы выглядела эта реализация в нашей саге о слайсах? API Redux Saga не предлагает специального метода для этого, но, как мы сейчас увидим, есть простой способ сделать это.

Во-первых, некоторый шаблонный код:

function* changePasswordSubmittedHandler() {
  // ...
}
function* changePasswordSaga() {
  yield all([
    takeLatest(CHANGE_PASSWORD_SUBMITTED, changePasswordSubmittedHandler)
  ])
}

Этот код будет выполняться каждый раз при обнаружении действия CHANGE_PASSWORD_SUBMITTED и отменит все предыдущие процессы, запущенные из предыдущих вызовов, следовательно, takeLatest .

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

function* changePasswordSubmittedHandler() {
  // ...
}
function* changePasswordFormFocusedHandler() {
  yield take(CHANGE_PASSWORD_FORM_FOCUSED)
  // ...
}

function* changePasswordSaga() {
  yield all([
    takeLatest(CHANGE_PASSWORD_SUBMITTED, changePasswordSubmittedHandler),
    changePasswordFormFocusedHandler()
  ])
}

Мы добавили changePasswordFormFocusedHandler в нашу сагу о слайсах. Вместо того, чтобы передавать его в качестве второго аргумента в takeLatest или takeEvery, мы просто запускаем функцию внутри массива, переданного в all. Затем внутри самого обработчика мы запускаем take в начале функции.

Что тут происходит? Запуск changePasswordFormFocusedHandler сам по себе, а не с takeLatest или takeEvery позволяет нам запустить обработчик один и только один раз. Оператор take в начале заставляет функцию приостановить. Только когда наше приложение обнаружит CHANGE_PASSWORD_FORM_FOCUSED, оно прекратит паузу и запустит остальную часть функции.

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

Ожидание первого завершенного действия с race

Иногда мы хотим прослушивать несколько действий и предпринимать определенные шаги в зависимости от того, какое из этих действий завершилось первым. Например, когда пользователь отправляет платеж за заказ, транзакция может завершиться успешно или завершиться неудачно по ряду причин. Для этого сценария мы обратимся к комбинатору эффектов race. Redux Saga называет race и all комбинаторами эффектов, потому что они оба принимают 1 или более эффектов и обрабатывают их одновременно.

Давайте посмотрим, как работает race:

function* paymentSubmittedHandler() {
  const { failed, finished, cancelled } = yield race({
    failed: take(PAYMENT_SUBMISSION_FAILED),
    finished: take(PAYMENT_SUBMISSION_FINISHED),
    cancelled: take(PAYMENT_SUBMISSION_CANCELLED)
  })
  
  if (finished) {
    // do something
  }

  if (cancelled || failed) {
    // do something
  }
}

Мы создаем гонку между 3 различными эффектами. Первый эффект для разрешения будет сохранен в значении ключа, поэтому, если finished будет первым, то у нас будет { finished: returnedResult } . Мы настраиваем условные блоки для обработки различных сценариев.

Это полезный подход к обработке асинхронных запросов. Мы используем гонки для следующих сценариев:

  • загрузка и инициализация SDK и библиотек
  • получение сегментов информации о пользователе
  • обработка редиректов
  • проверка поиска
  • управление токенами
  • События пользовательского интерфейса, подобные приведенному ниже:
const { closed } = yield race({
  confirmed: take(AGE_VERIFICATION_MODAL_CONFIRMED),
  closed: take(AGE_VERIFICATION_MODAL_CLOSED)
})

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

Совместное использование сервисов в вашем приложении с помощью getContext

Если мы хотим иметь доступ к определенным службам (или контекстам), которые можно использовать в наших сагах, мы можем использовать getContext. После некоторой быстрой настройки мы можем легко получить доступ к методам, которые могут вызывать API, LocalStorage, механизмы ведения журналов и промежуточное ПО.

import createSagaMiddleware from 'redux-saga'

const cookieStorage = {
  save: value => {
    // save the cookie
  }
}

const logger = {
  logInfo: info => {
    // log the info
  }
}

const sagaMiddleware = createSagaMiddleware({
  context: {
    cookieStorage,
    logger
  }
})

sagaMiddleware.run(rootSaga)

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

После этого мы можем получить доступ к этим контекстам в наших сагах слайсов с помощью getContext.

function* cookiePreferenceStorageSaga() {
  try {
    const { cookieStorage } = yield getContext('cookieStorage')

    const key = 'foobar'
 
    yield call(cookieStorage.save, key)
    yield put(cookiePreferenceStorageSucceeded())
  } catch (error) {
    const logger = yield getContext('logger')
    logger.logInfo('Cookie preference storage error', { error })
  }
}

Практически из любого места в наших сагах мы можем импортировать наши контексты, запустив getContext и передав контекст, к которому мы хотим получить доступ. Затем мы используем контекст, как нам нужно. Мы можем использовать call, если мы вызываем функцию, не являющуюся генератором, или мы можем просто вызвать метод, как в случае с logInfo.

Заключение

Эти изящные шаблоны служат надежным подходом к обработке динамической среды, такой как глобальное состояние массивного веб-приложения. Возможно, в следующем посте я задокументирую еще несколько шаблонов, которые мы используем в Just Eat Takeaway. Дайте мне знать, если вы хотите прочитать об этом!