ルートサガパターン
ルートサガは、複数のサガをsagaMiddlewareが実行するための単一のエントリポイントに集約します。
初心者向けチュートリアルでは、ルートサガは次のようなものになることが示されています。
export default function* rootSaga() {
yield all([
helloSaga(),
watchIncrementAsync()
])
// code after all-effect
}
これはルートを実装するいくつかの方法の1つです。ここでは、配列とともに`all`エフェクトが使用されており、サガは並列に実行されます。他のルート実装は、エラー処理やより複雑なデータフローの処理に役立ちます。
ノンブロッキングフォークエフェクト
コントリビューターの@slorberは、issue#760で、他の一般的なルート実装についていくつか言及しています。まず、チュートリアルのルートサガの動作と同様に動作する一般的な実装があります。
export default function* rootSaga() {
yield fork(saga1)
yield fork(saga2)
yield fork(saga3)
// code after fork-effect
}
3つのユニークな`yield fork`を使用すると、タスク記述子が3回返されます。アプリケーションにおける結果的な動作は、すべてのサブサガが同じ順序で開始され実行されることです。`fork`はノンブロッキングであるため、子サガが実行され続け、内部エフェクトによってブロックされている間、`rootSaga`は終了することができます。
`all`エフェクト1つと複数の`fork`エフェクトの違いは、`all`エフェクトはブロッキングであるため、allエフェクト後のコード(上記のコードのコメントを参照)はすべての子サガが完了した後に実行されるのに対し、`fork`エフェクトはノンブロッキングであるため、forkエフェクト後のコードは`fork`エフェクトのyield直後に実行されます。もう1つの違いは、`fork`エフェクトを使用するとタスク記述子を取得できるため、後続のコードでタスク記述子を使用してフォークされたタスクをキャンセル/結合できることです。
`all`エフェクト内のフォークエフェクトのネスト
const [task1, task2, task3] = yield all([ fork(saga1), fork(saga2), fork(saga3) ])
ルートサガを設計する際に、もう1つの一般的なパターンは、`all`エフェクト内に`fork`エフェクトをネストすることです。これにより、タスク記述子の配列を取得でき、各`fork`エフェクトはノンブロッキングであり、タスク記述子を同期的に返すため、`all`エフェクト後のコードはすぐに実行されます。
`fork`エフェクトが`all`エフェクトにネストされている場合でも、基盤となるforkQueueを通じて常に親タスクに接続されています。フォークされたタスクからのキャッチされないエラーは親タスクにバブルアップし、親タスク(およびそのすべての子タスク)を中断します。親タスクによってキャッチされることはありません。
`race`エフェクト内のフォークエフェクトのネストを避ける
// DO NOT DO THIS. The fork effect always wins the race immediately.
yield race([
fork(someSaga),
take('SOME-ACTION'),
somePromise,
])
一方、`race`エフェクト内の`fork`エフェクトは、ほとんどの場合バグです。上記のコードでは、`fork`エフェクトはノンブロッキングであるため、常にすぐにレースに勝ちます。
ルートを存続させる
実際には、これらの実装はあまり実用的ではありません。なぜなら、`rootSaga`は個々の子エフェクトまたはサガの最初のエラーで終了し、アプリケーション全体をクラッシュさせる可能性があるからです!特にAjaxリクエストは、アプリケーションがリクエストを行うエンドポイントの状態にアプリケーションを委ねることになります。
`spawn`は、子サガを親から切り離すエフェクトであり、親をクラッシュさせることなく失敗させることができます。もちろん、これは開発者としてのエラー処理の責任を免除するものではありません。実際、これにより開発者の視点から特定のエラーが隠され、後で問題が発生する可能性があります。
`spawn`エフェクトは、Reactのエラーバウンダリと似ていると考えることができます。これは、サガツリーのあるレベルで追加の安全対策として使用でき、失敗した機能を切り離し、アプリケーション全体がクラッシュするのを防ぐことができます。違いは、Reactエラーバウンダリのように`componentDidCatch`のような特別な構文がないことです。独自のエラー処理とリカバリコードを記述する必要があります。
export default function* rootSaga() {
yield spawn(saga1)
yield spawn(saga2)
yield spawn(saga3)
}
この実装では、1つのサガが失敗した場合でも、`rootSaga`と他のサガは停止しません。ただし、失敗したサガはアプリケーションの存続期間中は使用できなくなるため、これも問題になる可能性があります。
すべてを存続させる
場合によっては、サガが失敗した場合に再起動できることが望ましい場合があります。利点は、アプリケーションとサガは失敗後も動作を継続できることです(例:`yield takeEvery(myActionType)`のサガ)。しかし、すべてのサガを存続させるための包括的な解決策としてはお勧めしません。サガを健全かつ予測可能な方法で失敗させ、エラーを処理/記録する方が理にかなっている可能性が高いです。
例えば、@ajwhiteは、サガを存続させることが解決策よりも多くの問題を引き起こすシナリオを提示しました。
function* sagaThatMayCrash () {
// wait for something that happens _during app startup_
yield take('APP_INITIALIZED')
// assume it dies here
yield call(doSomethingThatMayCrash)
}
sagaThatMayCrashが再起動されると、再起動して、アプリケーションの起動時に一度だけ発生するアクションを待ちます。このシナリオでは、再起動しますが、回復しません。
しかし、サガの開始に役立つ特定の状況について、ユーザーの@granmoeはissue#570で次のような実装を提案しました。
function* rootSaga () {
const sagas = [
saga1,
saga2,
saga3,
];
yield all(sagas.map(saga =>
spawn(function* () {
while (true) {
try {
yield call(saga)
break
} catch (e) {
console.log(e)
}
}
}))
);
}
この戦略は、子サガを生成されたジェネレーター(ルート親から切り離されている)にマッピングし、`try`ブロックでサブタスクとしてサガを開始します。サガは終了するまで実行され、自動的に再起動されます。`catch`ブロックは、サガによってスローされ、終了された可能性のあるエラーを無害に処理します。