はじめに
アイスタイルでバックエンドを担当しているkuriyamayです。
最近RxJSを使用したプロダクトを作成し、本番環境へリリース・安定稼動するところまでやりました。
その間に様々なインプットがあったので、ここでアウトプットしていこうと思います。
本記事のRxJSはバージョン7.8.1を使用しています
RxJSとは
RxJSについては2024年のアドベントカレンダー記事をご覧ください。
オペレータの分類
RxJSオペレータは、Observableの生成や変換を行う関数で、大きく2種類に分かれます。
分類 | 説明 |
---|---|
Pipeable Operators |
.pipe() の中で使われ、既存の Observable を変換・操作するオペレータ。例: map , filter , mergeMap
|
Creation Operators |
非Observableな値(配列、数値、Promiseなど)から Observable を生成するオペレータ。例: of , from , interval
|
今回はPipeable OperatorsのMapオペレータについて説明していきます。
Mapオペレータとは
複数種類のMapオペレータがあり、その中でもどれを使えばいいか迷ってしまう下記3つのMapオペレータを取り上げます。
- mergeMap
- concatMap
- switchMap
mergeMap, concatMap, switchMap はすべて 「外側の Observable の各値を元に、新しい Observable を生成し、それを flatten(平坦化)して処理する」 という性質を持っています。
この3つはよく似た構文を持ちながらも振る舞いがまったく異なるため、混乱しがちです。
それぞれの特徴・ユースケース・ポイントを解説します。
mergeMap
並列実行(最も自由だが制御が難しい)
特徴
- すべての inner Observable を即座に subscribe
- 実行順序は保証されない
- 非同期処理を並列実行したいときに使う
ユースケース
- 複数APIの同時実行
- ユーザーの操作を漏れなく処理
import { of } from 'rxjs';
import { delay, mergeMap } from 'rxjs/operators';
of('A', 'B', 'C').pipe(
mergeMap(value =>
of(`done: ${value}`).pipe(delay(1000))
)
).subscribe(console.log);
// 出力(約1秒後)
// done: A
// done: B
// done: C
// 順番は保証されません(環境やタイミングにより done: B → A → C などもあり得ます)
ポイント
- A, B, C のそれぞれの非同期処理が同時に開始される
- delay(1000) によりそれぞれが 1 秒後に完了
- 処理が並列のためほぼ同時に全て出力される
concatMap
順序保証(処理は1つずつ)
特徴
- inner Observable の完了を待ってから次を実行
- 順番を保証する
- 並列性はなく、直列処理
ユースケース
- 処理に順序が必要なAPI呼び出し
- キュー処理など、1つずつ確実に処理したいとき
import { of } from 'rxjs';
import { delay, concatMap } from 'rxjs/operators';
of('A', 'B', 'C').pipe(
concatMap(value =>
of(`done: ${value}`).pipe(delay(1000))
)
).subscribe(console.log);
// 出力
// (done: A) ← 1秒後に出力
// (done: B) ← さらに1秒後(2秒時点)
// (done: C) ← さらに1秒後(3秒時点)
ポイント
- 各値が 順番に処理され、完了するまで次の処理は始まらない
- delay(1000) により1件1秒なので、全体で約3秒かかる
- 順序が非常に重要な処理(例:ログ記録、順次API実行など)に適している
switchMap
最新だけ残す(キャンセル可能)
特徴
- 前の処理をキャンセルして、最新の Observable のみ実行
- 最も UX 向け
- 古いリクエストをキャンセルしたいときに便利
ユースケース
- 検索バーのリアルタイム補完
- ボタン連打などの 最新操作のみ反映
import { of } from 'rxjs';
import { delay, switchMap } from 'rxjs/operators';
of('A', 'B', 'C').pipe(
switchMap(value =>
of(`done ${value}`).pipe(delay(1000))
)
).subscribe(console.log);
// 出力(約1秒後)
// done C
補足
- of('A', 'B', 'C') は 即座に連続で値を流す
- switchMap は、毎回新しい Observable を生成し、前のObservableはキャンセル
- 各処理は delay(1000) によって1秒後に出力される予定
- しかし次の値がすぐ来るため、前のObservableは完了せずにキャンセルされる
- 最後の C だけは、後続がないためキャンセルされず完了 → 出力される
もっと分かりやすく表すとこのような流れです
switchMap(A) → キャンセルされる
switchMap(B) → キャンセルされる
switchMap(C) → ✅ 出力される(後続がない)
ポイント
- switchMap は、常に最新のイベントにのみ反応する
- 遅い非同期処理を抱えていると、途中でキャンセルされて出力されないケースが多い
- UIの検索欄やボタン連打対策に最適
📌 switchMap
の最大の強みは「非同期処理のキャンセル性」です。
Promise
や async/await
だと一度始まった処理は、たとえ新しい入力が来ても止められず、完了を待つしかありません。
そのため、たとえばユーザーがボタンを連打したり、検索を連続で打ち込んだ場合に不要なリクエストもすべて実行されてしまうリスクがあります。
switchMap
は Promise
や async/await
にはないRxJS特有の利点であり、リアクティブなアプリケーション設計において非常に重要な特性です。
振る舞い比較表
オペレータ | 並列実行 | 処理順序保証 | キャンセル機能 | 主な用途 |
---|---|---|---|---|
mergeMap |
✅ | ❌ | ❌ | 並列実行が必要なAPI |
concatMap |
❌ | ✅ | ❌ | 順序付きの直列API処理 |
switchMap |
❌ | ❌ | ✅ | ユーザー操作など最新の値だけ処理 |
オペレータの使い分けガイド
-
✅ 並列で一気に処理したい →
mergeMap
例:データ一覧に対する一括取得処理など -
✅ 順番通りに一つずつ処理したい →
concatMap
例:ログを順番に保存する、メッセージ送信など -
✅ ユーザー操作で「最新だけを処理したい」 →
switchMap
例: 検索バー、ボタン連打、リアルタイムリクエスト
まとめ
mergeMap, concatMap, switchMap はよく似ていますが、選択を誤るとバグやUX低下の原因になります。
それぞれの動作特性を理解し、「いつ何を使うか」を判断できるようになることで、RxJSの力を最大限に活かせるようになります。