switchMap1でObservableを切り替える前の値もまとめて受け取れるようにしたい、もしくはcombineLatestのようなオペレータで実行順序を制御したいと思ったことはないでしょうか。
例えば以下のようにリクエストAの結果がリクエストBのパラメータとして必要な場合を考えます。
type A = { prop: string };
function requestA(): Observable<A>;
function requestB(prop: string): Observable<B>;
この場合は単にrequestBの結果を取得したい場合はそのままswitchMapを使うといいです。
requestA().pipe(
switchMap(a => requestB(a.prop))
)
.subscribe(b => /* bを使った何か */);
しかし、リクエストAの結果もキャッシュしてコンポーネントのテンプレートに渡したい場合などでは、switchMap内もしくはtapオペレータでインスタンス変数に代入する必要があります。
requestA().pipe(
tap(a => (this.a = a)),
switchMap(a => requestB(a.prop))
)
.subscribe(b => {
/* this.aを使った何か */
/* bを使った何か */
});
このようなシンプルな例だとこのままの実装で問題ないのですが、リクエストを受け取ったあとに必要な処理が膨れ上がってきたり、依存関係のあるリクエストの数が増えると読みづらいコードになってしまいます。
一方、combineLatestやforkJoinのようなオペレータは複数のリクエスト結果をタプルで受け取ることができますが、リクエストの呼び出し順を指定したい場合には使えません。
const a$ = requestA();
const b$ = requestB(a.prop); // aをどうやってa$から取り出す?
combineLatest([a$, b$]).subscribe(([a, b]) /* 型は[A, B]のタプル */ => {
/* ... */
}
解決策
そこで紹介するのはswitchMapのようにObservableを切り替えつつ、入力元の値も含めて返す方法です。
以下のような実装をします。
requestA().pipe(
switchMap(a => requestB(a.prop).pipe(
map(b => [a, b] as const)
)
).subscribe(([a, b]) => {
/* ... */
});
元々switchMapにはquerySelectorという引数でコールバックを指定できていましたが、RxJSからはこのquerySelectorはdeprecatedになっているので、代わりに上記のようにswitchMap内のmapで変換する必要があります。
そしてswitchMap内のconst assertionで型をタプルにしています。const assertionがないとsubscribeのコールバック引数の型が(A | B)[]
のようになってしまいます。
ちなみにタプルではなくshorthand propertyとオブジェクトの分割代入で次のようにも書けます。
requestA().pipe(
switchMap(a => requestB(a.prop).pipe(
map(b => ({a, b}))
)
).subscribe(({a, b}) => {
/* ... */
});
-
この記事ではswitchMapを例に説明していますが、mergeMap(flatMap), concatMap, exhaustMapでも同じです。 ↩