← 解説「よく使うRxJSオペレーター — map / filter / switchMap」に戻る
サンプルコードで身につける: よく使うRxJSオペレーター — map / filter / switchMap
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1map — 届いた値を別の形に変換する
APIが返す消費税抜きの価格一覧を、mapで税込価格に変換する例です。pipeという加工ラインにmapを1つ置くと、流れてくる値すべてがその変換を通ってから届きます。元のObservableは変更されず、新しい流れが作られる点もポイントです。
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AsyncPipe } from '@angular/common';
import { map } from 'rxjs';
@Component({
selector: 'app-price-list',
imports: [AsyncPipe],
template:
'@for (price of taxedPrices$ | async; track $index) {' +
' <p>{{ price }} 円(税込)</p>' +
'}',
})
export class PriceListComponent {
private http = inject(HttpClient);
// 加工ライン: 税抜価格の配列 → 税込価格の配列
taxedPrices$ = this.http
.get<number[]>('/api/prices')
.pipe(map((prices) => prices.map((p) => Math.floor(p * 1.1))));
}
2filter — 条件を満たす値だけを通す
検索欄の入力のうち、2文字以上のものだけを通すfilterの例です。filterは値を変換せず、条件に合わない値を加工ラインの途中で捨てます。「短すぎるキーワードでは検索しない」という実務でよくある要件が1行で書けます。
import { Component } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Subject, filter } from 'rxjs';
@Component({
selector: 'app-keyword-log',
imports: [AsyncPipe],
template:
'<input (input)="onInput($event)" placeholder="2文字以上で反応">' +
'<p>検索対象: {{ validKeyword$ | async }}</p>',
})
export class KeywordLogComponent {
private keyword$ = new Subject<string>();
// 2文字未満の入力はラインの途中で捨てられる
validKeyword$ = this.keyword$.pipe(filter((k) => k.length >= 2));
onInput(e: Event) {
this.keyword$.next((e.target as HTMLInputElement).value);
}
}
3filter + map — オペレーターをつないで加工ラインを作る
pipeの中にfilterとmapを順に並べ、「在庫のある商品だけを通し、表示用の文字列に変換する」ラインを組んだ例です。上から順に加工が適用されるので、処理の流れがコードの並びと一致して読みやすくなります。オペレーターは小さな部品をつないで使うのが基本です。
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AsyncPipe } from '@angular/common';
import { map } from 'rxjs';
type Item = { name: string; stock: number };
@Component({
selector: 'app-stock-list',
imports: [AsyncPipe],
template:
'@for (label of labels$ | async; track label) { <p>{{ label }}</p> }',
})
export class StockListComponent {
private http = inject(HttpClient);
// ライン: 全商品 → 在庫ありに絞る → 表示用文字列へ変換
labels$ = this.http.get<Item[]>('/api/items').pipe(
map((items) => items.filter((i) => i.stock > 0)),
map((items) => items.map((i) => i.name + '(残り' + i.stock + '点)'))
);
}
4switchMap — 検索語が変わったら前のリクエストを捨てる
入力のたびにAPIを呼ぶインクリメンタルサーチの定番例です。switchMapは新しい検索語が流れてくると、進行中だった前のリクエストをキャンセルして新しいリクエストに乗り換えます。古い結果が遅れて届いて画面を上書きする、という不具合が構造的に起きません。
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AsyncPipe } from '@angular/common';
import { Subject, switchMap } from 'rxjs';
type Book = { id: number; title: string };
@Component({
selector: 'app-book-search',
imports: [AsyncPipe],
template:
'<input (input)="onInput($event)" placeholder="書名で検索">' +
'@for (b of books$ | async; track b.id) { <p>{{ b.title }}</p> }',
})
export class BookSearchComponent {
private http = inject(HttpClient);
private keyword$ = new Subject<string>();
// 新しい検索語が来たら、前のリクエストは捨てて乗り換える
books$ = this.keyword$.pipe(
switchMap((q) =>
this.http.get<Book[]>('/api/books', { params: { q } })
)
);
onInput(e: Event) {
this.keyword$.next((e.target as HTMLInputElement).value);
}
}
5debounceTime + distinctUntilChanged で検索を仕上げる
switchMapの検索に、入力が落ち着くまで300ms待つdebounceTimeと、同じ値の連続を捨てるdistinctUntilChangedを加えた実務の完成形です。1文字ごとにAPIを叩かなくなるため、サーバーの負荷もちらつきも減ります。この4オペレーターの組み合わせは検索画面の定石として丸ごと覚えて構いません。
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AsyncPipe } from '@angular/common';
import {
Subject,
debounceTime,
distinctUntilChanged,
filter,
switchMap,
} from 'rxjs';
type Shop = { id: number; name: string };
@Component({
selector: 'app-shop-search',
imports: [AsyncPipe],
template:
'<input (input)="onInput($event)" placeholder="店舗名で検索">' +
'@for (s of shops$ | async; track s.id) { <p>{{ s.name }}</p> }',
})
export class ShopSearchComponent {
private http = inject(HttpClient);
private keyword$ = new Subject<string>();
shops$ = this.keyword$.pipe(
debounceTime(300), // 入力が300ms止まるまで待つ
distinctUntilChanged(), // 直前と同じ値なら捨てる
filter((q) => q.length >= 2), // 短すぎる検索語は捨てる
switchMap((q) =>
this.http.get<Shop[]>('/api/shops', { params: { q } })
)
);
onInput(e: Event) {
this.keyword$.next((e.target as HTMLInputElement).value);
}
}