Если вы пробовали другое промежуточное ПО в Redux, например Redux Thunk, и обнаружили, что их трудно читать и поддерживать, возможно, стоит попробовать r edux-saga.

Многие наши проекты React / Redux на 20 спицах требуют, чтобы при выполнении пользователем одного действия происходило много разных вещей - необходимо получить новую информацию, обновить аудит, сохранить ввод и т. Д. Использование саг для выполнения эти действия сделали наш код более понятным.

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

Генераторы

Не вдаваясь в подробности, генераторные функции - это, по сути, функции, которые можно приостанавливать и возобновлять. Функции генератора приостанавливаются с помощью ключевого слова yield и возобновляются вызовом next() функции. Redux-saga позаботится о приостановке и возобновлении функций генератора за вас. Ваша задача - предоставить правильные возвращаемые значения для каждого блока yield, используя собственные вспомогательные функции redux-saga, чаще всего put, call и fork. (Эти функции в документации называются создателями эффектов.)

Функции генератора указываются с помощью function*. Вот пример одной из наших функций генератора redux-saga:

function* beginSession() {
  yield fork(fetchRate)
  yield call(createAudit)
  yield put({ type: 'SET_LAYOUT', payload: 'application' })
}

Наибольшее удобство redux-saga - это возможность запускать параллельные задачи и упорядочивать все эти действия.

Например, в приведенной выше саге на самом деле одновременно выполняются две задачи. Первая задача - это fetchRate, которая выполняется независимо с использованиемfork. Вторая задача - сначала вызвать createAudit с помощью call, а затем отправить действие SET_LAYOUT после завершения createAudit с помощью put. Одна из этих задач может быть завершена раньше другой, и это нормально, потому что они не зависят друг от друга.

Использование генераторов с вызовами API

Вот пример саги, которая обрабатывает возвращаемое значение из вызова API:

export function* fetchRate(){
  try {
    yield put({type: 'FETCH_RATE_REQUESTED'})
    const response = yield call(getRate)
    
    yield put({
      type: 'FETCH_RATE_SUCCEEDED',
      payload: response.current_rate
    })
  } catch (e) {
    yield put({
      type: 'REQUEST_FAILED',
      payload: { message: e.message }
    })
  }
}

Здесь следует отметить несколько моментов. Во-первых, мы помещаем вызов API в блок try / catch. Во-вторых, мы используем call для вызова api, что означает, что следующий блок yield, использующий данные ответа, не будет выполнен, пока вызов api не будет разрешен. Наконец, мы отправляем действия Redux, которые объявляют о начале запроса, его успешности и потенциальной неудаче. Это очень полезно для отладки.

Обычно мы используем fetch для выполнения наших вызовов API. Мы создали отдельные инструменты api вне наших саг, которые выглядят следующим образом:

function api(ourFetch) {
  return ourFetch.then(function (resp) {
      return resp.json()
    }).then(function (json) {
      // any custom error handling here
      if (json.session_error) throw new Error(json.session_error)
      return json
    })
}
export const getRate = function() {
  return api(fetch('http://myendpoint.com/rate', {
    method: 'GET',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    }
  }))
}

Это та часть, которая сбила меня с толку насчет саг, когда я впервые их изучал: возвращаемое значение того, что вы даете call Saga, должно быть разрешенным обещанием. Разрешение ваших выборок за пределами ваших саг делает их намного более читаемыми, модульными и тестируемыми.

Саги о тестировании

Я обнаружил, что саги о тестировании очень просты и синхронны, используя пакет под названием redux-saga-testing. Redux-saga-testing берет на себя ваши тесты и передает результирующий объект-генератор каждого yield блока для каждого теста. (Обратите внимание на то, как каждый it соответствует каждому yield в fetchRate выше.)

Мне нравится этот пакет, потому что все, что вам нужно сделать, чтобы имитировать возвращаемое значение для yield (например, ваш возврат из вызова API), - это вернуть фиктивное значение для it:

// Tests Using Mocha and Chai
import sagaHelper from 'redux-saga-testing'
describe('fetchRate() on success', () => {
  const it = sagaHelper(fetchRate())
  it('notifies of api request', result => {
    expect(result).to.eql(put({type: 'FETCH_RATE_REQUESTED'}))
  })
  it('calls api', result => {
    expect(result).to.eql(call(getRate))
    // mock response from api
    return { current_rate: 100 }
  })
  it('updates store with current rate', result => {
    expect(result).to.eql(put({
      type: 'FETCH_RATE_SUCCEEDED',
      payload: 100
    }))
  })
})

Нет необходимости в каких-либо заглушках или инструментах для извлечения макетов!

Доступ к данным состояния в сагах

Иногда в саге вам понадобится доступ к данным состояния из хранилища Redux. В Redux-saga есть метод для этого под названием select. Вот пример саги, в которой используется select:

const getAudit = (state) => state.audit
const getCurrentStep = (state) => state.layout.currentStep
export function* updateAudit(action) {
  try {
    yield put({ type: 'UPDATE_AUDIT_REQUESTED'})
    const audit = yield select(getAudit)
    const currentStep = yield select(getCurrentStep)
    let data = {
      ...audit,
      ...action.payload,
      last_step: currentStep
    }
    const response = yield call(Api.updateAuditPost, data)
    yield put({
      type: 'UPDATE_AUDIT_SUCCEEDED',
      payload: response.audit
    })
} catch (e) {
    
    yield put({
      type: Requests.REQUEST_FAILED,
      payload: { message: e.message }
    })
  }
}

Обратите внимание, что вы должны вызывать select в блоке yield так же, как вы делаете с call, put и fork.

Предложения по использованию

Redux-saga - это промежуточное ПО, что означает, что саги прерывают определенные действия Redux, которые вы укажете (см. Документацию для takeEvery и takeLatest). Если вы используете отдельные файлы для определения редукторов, действий и саг, может быть сложно увидеть, какие действия перехватываются промежуточным программным обеспечением. Вот почему хорошо иметь какое-то соглашение об именах для всех типов действий, которые попадают в саги. Я начал добавлять _SAGA к этим типам действий (например, FETCH_RATE_SAGA). В документации используется _REQUESTED, но я обнаружил, что это соглашение слишком противоречит другим действиям, которые я отправлял для сетевых запросов. Помидор, томахто.

Вывод

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

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