将来のアクションのプル
これまで、私たちは受信したアクションごとに新しいタスクを生成するためにヘルパーエフェクトtakeEvery
を使用してきました。これは、redux-thunk
の動作をいくらか模倣しています。たとえば、コンポーネントがfetchProducts
アクションクリエイターを呼び出すたびに、アクションクリエイターは制御フローを実行するためにthunkをディスパッチします。
実際には、takeEvery
は、より低レベルで強力なAPIの上に構築された内部ヘルパー関数の単なるラッパーエフェクトです。このセクションでは、アクションの監視プロセスを完全に制御できるようにすることで、複雑な制御フローを構築できる新しいエフェクトtake
を紹介します。
基本的なロガー
ストアにディスパッチされたすべてのアクションを監視し、それらをコンソールにログを記録するSagaの基本的な例を見てみましょう。
takeEvery('*')
(ワイルドカード*
パターンを使用)を使用すると、タイプに関係なく、ディスパッチされたすべてのアクションをキャッチできます。
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)
})
}
次に、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)
}
}
take
は、以前見たcall
およびput
とよく似ています。これは、ミドルウェアに特定のアクションを待つように指示する別のコマンドオブジェクトを作成します。call
エフェクトの結果の動作は、Promiseが解決されるまでミドルウェアがジェネレーターを中断する場合と同じです。take
の場合、一致するアクションがディスパッチされるまでジェネレーターは中断されます。上記の例では、アクションがディスパッチされるまでwatchAndLog
は中断されます。
無限ループwhile (true)
を実行していることに注意してください。これは、完了まで実行する動作がないジェネレーター関数であることを思い出してください。私たちのジェネレーターは、アクションが発生するのを待って、各反復でブロックします。
take
を使用すると、コードの記述方法に微妙な影響があります。takeEvery
の場合、呼び出されたタスクは、いつ呼び出されるかを制御できません。一致するアクションごとに何度も何度も呼び出されます。また、監視を停止するタイミングも制御できません。
take
の場合、制御は反転されます。アクションがハンドラータスクにプッシュされるのではなく、Saga自体がアクションをプルしています。Sagaが通常関数呼び出しaction = getNextAction()
を実行しているかのように見えます。この関数はアクションがディスパッチされたときに解決されます。
この制御の反転により、従来のアプローチであるプッシュでは実現が難しい制御フローを実装できます。
基本的な例として、Todoアプリケーションで、ユーザーアクションを監視し、ユーザーが最初の3つの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'})
}
while (true)
の代わりに、3回だけ反復するfor
ループを実行しています。最初の3つのTODO_CREATED
アクションを取得した後、watchFirstThreeTodosCreation
はアプリケーションにおめでとうメッセージを表示させてから終了させます。これは、ジェネレーターがガベージコレクションされ、これ以上の監視が行われないことを意味します。
プルアプローチのもう1つの利点は、使い慣れた同期スタイルを使用して制御フローを記述できることです。たとえば、LOGIN
とLOGOUT
の2つのアクションでログインフローを実装するとします。takeEvery
(またはredux-thunk
)を使用すると、LOGIN
用とLOGOUT
用の2つの別々のタスク(またはthunk)を記述する必要があります。
その結果、ロジックが2つの場所に分散されます。私たちのコードを読んでいる人がそれを理解するためには、2つのハンドラーのソースを読み、両方のロジック間のリンクを頭の中で作成する必要があります。つまり、コードのさまざまな場所に配置されたロジックを頭の中で正しく並べ替えて、フローのモデルを頭の中で再構築する必要があるということです。
プルモデルを使用すると、同じアクションを繰り返し処理する代わりに、同じ場所にフローを記述できます。
function* loginFlow() {
while (true) {
yield take('LOGIN')
// ... perform the login logic
yield take('LOGOUT')
// ... perform the logout logic
}
}
loginFlow
Sagaは、予期されるアクションシーケンスをより明確に伝えています。LOGIN
アクションの後に必ずLOGOUT
アクションが続き、LOGOUT
の後に必ずLOGIN
が続くことを認識しています(優れたUIは、予期しないアクションを非表示または無効にすることにより、アクションの一貫した順序を常に強制する必要があります)。