Если вы пробовали другое промежуточное ПО в 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.