Получение будущих действий
До сих пор мы использовали helper Effect takeEvery
, чтобы создать новый таск для каждого входящего action. Это немного напоминает поведение redux-thunk
: например, каждый раз, когда компонент, вызывает fetchProducts
Action Creator, Action Creator отправляет thunk для выполнения потока управления.
В действительности, takeEvery
- всего лишь враппер для внутренней вспомогательной функции, созданной на основе более низкого уровня и более мощного API. В этом разделе мы увидим новый Effect take
, который позволит построить сложный поток управления, позволяя полностью контролировать процесс наблюдения за action.
Простой логгер action-ов
Давайте рассмотрим простой пример Саги, которая следит за всеми действиями, отправленными в Store, и выводит их в консоль.
Используя takeEvery('*')
(с аргументом *
) мы можем отловить все отправленные action независимо от их типов.
import { select, takeEvery } from 'redux-saga/effects'
function* watchAndLog() {
yield takeEvery('*', function* logger(action) {
const state = yield select()
console.log('action', action)
console.log('state after', state)
})
}
Теперь давайте посмотрим, как использовать Effect take
для реализации того же потока, что и выше
import { select, take } from 'redux-saga/effects'
function* watchAndLog() {
while (true) {
const action = yield take('*')
const state = yield select()
console.log('action', action)
console.log('state after', state)
}
}
Effecttake
похож на call
и на put
который мы видили раньше. Он создает еще один командный объект,1 который сообщает middleware, что-бы тот ждал определенного действия.
Результирующее поведение Effect-а call
аналогично тому, когда midleware приостанавливает генератор до того пока Promise
не вернет результат. В приведенном выше примере watchAndLog
приостанавливается до тех пор пока не будет диспачено какое нибудь действие.
Обратите внимание на то, как мы запускаем бесконечный цикл while (true)
. Помните, что это функция Генератор, которая работает не также как обычные функции. Наш генератор будет останавливатся на каждой итерации в ожидании пока не произайдет диспатч.
Использование take
оказывает тонкое влияние на то, как мы пишем наш код. В случае takeEvery
вызываемые таски не контролируют, когда они будут вызваны. Они будут вызываться снова и снова для каждого соответствующего action. Они также не контролируют, когда остановить наблюдение.
В случае с take
управление - инвертируется. Instead of the actions being pushed to the handler tasks, the Saga is pulling the action by itself. Это выглядит так, как будто Saga выполняет нормальную функцию action = getNextAction()
, которая будет решать, когда action будет диспатчен.
Эта инверсия управления позволяет нам реализовать потоки управления, которые нетривиальны для традиционного подхода push.
В качестве простого примера предположим, что в нашем приложении Todo мы хотим посмотреть действия пользователя и показать поздравительное сообщение после того, как пользователь создал свои первые три заметки.
import { take, put } from 'redux-saga/effects'
function* watchFirstThreeTodosCreation() {
for (let i = 0; i < 3; i++) {
const action = yield take('TODO_CREATED')
}
yield put({type: 'SHOW_CONGRATULATION'})
}
Instead of a while (true)
we're running a for
loop which will iterate only three times. After taking the first three TODO_CREATED
actions, watchFirstThreeTodosCreation
will cause the application to display a congratulation message then terminate. This means the Generator will be garbage collected and no more observation will take place.
Another benefit of the pull approach is that we can describe our control flow using a familiar synchronous style. For example, suppose we want to implement a login flow with two actions LOGIN
and LOGOUT
. Using takeEvery
(or redux-thunk
) we'll have to write two separate tasks (or thunks): one for LOGIN
and the other for LOGOUT
.
The result is that our logic is now spread in two places. In order for someone reading our code to understand it, they would have to read the source of the two handlers and make the link between the logic in both in their head. In other words, it means they would have to rebuild the model of the flow in their head by rearranging mentally the logic placed in various places of the code in the correct order.
Using the pull model we can write our flow in the same place instead of handling the same action repeatedly.
function* loginFlow() {
while (true) {
yield take('LOGIN')
// ... выполняем логику авторизации
yield take('LOGOUT')
// ... выполняем логику выхода
}
}
loginFlow
более четко передает ожидаемую последовательность действий. Он знает, что за action LOGIN
всегда следует действие LOGOUT
и что LOGOUT
всегда следует LOGIN
(хороший пользовательский интерфейс должен всегда обеспечивать последовательный порядок действий, скрывая или отключая неожиданное фсешщт).
1. It creates another command object ↩