はじめに
Angular技術要素解説シリーズ、第6弾です。
今回は、サーバーとのHTTP通信を担うHttpClientと、その裏側で使われているRxJSを解説します。
以前解説したRxJavaによるリアクティブプログラミングのJavaScript版にあたるライブラリで、AngularのHTTP通信はこのRxJSの考え方を土台にしています。
この記事で扱う技術要素
- HttpClient
- Observable(オブザーバブル)
- RxJSの基本オペレーター(
map・filter・catchErrorなど) - Interceptor(インターセプター)
HttpClientの基本
Angularは、HTTP通信のためのHttpClientを標準搭載しています。
まずは基本設定から見ていきます。
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app.component';
bootstrapApplication(AppComponent, {
providers: [provideHttpClient()], // HttpClientを使えるようにする
});
GETリクエストの例
// user.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
interface User {
id: number;
name: string;
email: string;
}
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
private apiUrl = '/api/users';
getUsers(): Observable<User[]> {
// HttpClientのget()は、即座にObservableを返す(この時点ではまだ通信は始まっていない)
return this.http.get<User[]>(this.apiUrl);
}
}
// user-list.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { UserService } from './user.service';
@Component({
selector: 'app-user-list',
standalone: true,
template: `
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }} ({{ user.email }})</li>
}
</ul>
`,
})
export class UserListComponent implements OnInit {
private userService = inject(UserService);
users: User[] = [];
ngOnInit(): void {
// subscribe()を呼んで初めて、実際のHTTPリクエストが送信される
this.userService.getUsers().subscribe(users => {
this.users = users;
});
}
}
http.get()を呼んだ時点では、まだ実際の通信は始まっていません。
Observableは「値が将来流れてくる約束」を表しているだけで、subscribe()(購読)して初めて実際の処理が実行されるという点が、Promiseとの大きな違いです。
Observableとは
Observableは、RxJSにおける「時間とともに値が流れてくるストリーム」を表す型です。
以前解説したRxJavaのObservableと、考え方は基本的に同じです。
import { Observable } from 'rxjs';
// 手動でObservableを作る例(実際にはHttpClient等が内部で生成してくれる)
const numberStream$ = new Observable<number>(subscriber => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
subscriber.complete(); // ストリームの終了を通知する
});
numberStream$.subscribe({
next: (value) => console.log('値:', value),
complete: () => console.log('完了しました'),
});
実行結果:
値: 1
値: 2
値: 3
完了しました
変数名の末尾に$(ドル記号)を付けるのは、「この変数はObservableである」ことを示すRxJSコミュニティの慣習です。
必須ではありませんが、Angularプロジェクトのコードでもよく見かけます。
パイプとオペレーターでストリームを加工する
RxJSの真価は、Observableをオペレーターでつなぎ合わせて加工できる点にあります。
pipe()メソッドで、複数のオペレーターを連結します。
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, filter, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
interface User {
id: number;
name: string;
isActive: boolean;
}
@Injectable({ providedIn: 'root' })
export class UserService {
private http = inject(HttpClient);
getActiveUserNames(): Observable<string[]> {
return this.http.get<User[]>('/api/users').pipe(
// filterで有効なユーザーだけに絞り込む(配列全体に対する処理はこの時点ではまだできないので、実際はArray側で行うことが多いが、概念として例示)
map(users => users.filter(user => user.isActive)),
// mapで、ユーザーオブジェクトの配列を名前の配列に変換する
map(users => users.map(user => user.name)),
// catchErrorでエラー発生時に、空配列を返してストリームを継続させる
catchError(error => {
console.error('ユーザー取得に失敗しました', error);
return of([]); // ofは「単一の値をそのままObservableにする」関数
})
);
}
}
| オペレーター | 役割 |
|---|---|
map |
流れてくる値を、別の値に変換する |
filter |
条件に合う値だけを通過させる |
catchError |
エラーが発生した際に、代わりの値やストリームを返す |
tap |
値の中身は変えずに、ログ出力などの副作用だけを行う |
switchMap |
前のストリームの結果をもとに、新しいストリームに乗り換える(検索候補の逐次取得など) |
debounceTime |
一定時間、値の発行がなければ待ってから次に進む(入力補完の連打防止など) |
実践例:検索ボックスの入力に応じてAPIを呼ぶ
switchMapとdebounceTimeを組み合わせた、実務でよくある例を見てみます。
import { Component, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
@Component({
selector: 'app-search-box',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<input [formControl]="searchControl" placeholder="ユーザーを検索">
<ul>
@for (result of searchResults; track result.id) {
<li>{{ result.name }}</li>
}
</ul>
`,
})
export class SearchBoxComponent {
private http = inject(HttpClient);
searchControl = new FormControl('');
searchResults: { id: number; name: string }[] = [];
constructor() {
this.searchControl.valueChanges
.pipe(
debounceTime(300), // 入力が300ms止まってから処理する(連打防止)
distinctUntilChanged(), // 直前と同じ値なら何もしない
switchMap(keyword =>
// 新しい検索が来たら、前の検索リクエストは自動的にキャンセルされる
this.http.get<{ id: number; name: string }[]>(`/api/users?q=${keyword}`)
)
)
.subscribe(results => {
this.searchResults = results;
});
}
}
このコードは、debounceTime・distinctUntilChanged・switchMapという3つのオペレーターを組み合わせるだけで、「入力のたびに無駄なAPI呼び出しをしない」「古い検索結果が新しい結果を追い越して表示される事故を防ぐ」という、地味に実装が面倒な処理を簡潔に実現しています。
これは、コールバックやasync/awaitだけで同じことを書こうとすると、キャンセル処理などがかなり複雑になる典型例です。
Interceptor(インターセプター)
インターセプターは、すべてのHTTPリクエスト・レスポンスに対して、共通の処理を差し込む仕組みです。
認証トークンの付与や、エラーの一括処理によく使われます。
// auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.getToken();
if (token) {
// すべてのリクエストのヘッダーに、自動的に認証トークンを付与する
const clonedRequest = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
return next(clonedRequest);
}
return next(req);
};
// main.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './auth.interceptor';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withInterceptors([authInterceptor])), // インターセプターを登録
],
});
このインターセプターを登録しておくと、アプリ内のあらゆるhttp.get()・http.post()呼び出しに対して、個別にトークンを付与するコードを書かなくても、自動的に認証ヘッダーが付与されるようになります。
同様の仕組みで、通信エラー発生時のログ記録や、共通のエラーダイアログ表示なども一箇所に集約できます。
Signalsとの使い分け
以前の記事で解説したSignalsが普及したことで、「単純なコンポーネントの状態管理」はSignalへの置き換えが進んでいますが、HTTP通信のような非同期のストリーム処理では、依然としてRxJSの役割が大きいのが実情です。
近年は、toSignal()関数を使ってObservableの結果をSignalに変換し、両者を橋渡しする書き方もよく使われます。
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-user-list',
standalone: true,
template: `
<ul>
@for (user of users(); track user.id) {
<li>{{ user.name }}</li>
}
</ul>
`,
})
export class UserListComponent {
private http = inject(HttpClient);
// ObservableをSignalに変換する。以後はテンプレートでusers()として扱える
users = toSignal(this.http.get<User[]>('/api/users'), { initialValue: [] });
}
まとめ
| 用語 | 役割 |
|---|---|
| HttpClient | Angular標準搭載の、HTTP通信を行うためのサービス |
| Observable | 時間とともに値が流れてくるストリームを表す、RxJSの中心的な型 |
| オペレーター(map/filter/switchMap等) | Observableを加工・変換するための関数群 |
| Interceptor | すべてのHTTPリクエスト・レスポンスに共通処理を差し込む仕組み |
toSignal() |
ObservableをSignalに変換し、両者を連携させるための関数 |
次回は、ユーザー入力を扱うフォームについて解説します。
学習に使った書籍
本記事の内容整理にあたっても、以下の書籍を参考にしました。
- Angular 超入門 第2版 [ 掌田津耶乃 ]
- Angularアプリケーションプログラミング 開発の勘所と手順をしっかりマスター [ 山田祥寛 ]
- Angular Webアプリ開発スタートブック [ 大澤文孝 ]
※上記リンクはアフィリエイトリンクを含みます。