← 解説「toSignal と toObservable — 2つの世界の橋渡し」に戻る
サンプルコードで身につける: toSignal と toObservable — 2つの世界の橋渡し
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1toSignal — HttpClientの結果をシグナルにする
HttpClientが返すObservableをtoSignalでシグナルに変換する基本形です。テンプレートではusers()と普通のシグナルとして読むだけで、asyncパイプもsubscribeも登場しません。購読の解除もtoSignalが自動で行います。
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
type User = { id: number; name: string };
@Component({
selector: 'app-member-list',
// シグナルなので () で呼ぶだけ
template: '@for (u of users(); track u.id) { <p>{{ u.name }}</p> }',
})
export class MemberListComponent {
private http = inject(HttpClient);
// Observable → シグナル。値が届くたびにシグナルが更新される
users = toSignal(this.http.get<User[]>('/api/users'), {
initialValue: [] as User[],
});
}
2initialValueなしのtoSignalとundefined
initialValueを指定しない場合、最初の値が届くまでシグナルはundefinedを返すため、型はUser | undefinedになります。テンプレートでは@ifで「まだ届いていない」状態を分けて扱います。undefinedを意識したくない一覧系では初期値に空配列を渡すのが定番です。
import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal } from '@angular/core/rxjs-interop';
type User = { id: number; name: string };
@Component({
selector: 'app-me',
// 届くまでは undefined なので @if で分岐する
template:
'@if (me(); as user) { <p>{{ user.name }} さん</p> }' +
' @else { <p>読み込み中...</p> }',
})
export class MeComponent {
private http = inject(HttpClient);
// initialValue なし → 型は Signal<User | undefined>
me = toSignal(this.http.get<User>('/api/me'));
}
3toObservable — シグナルの変化を流れとして取り出す
シグナルの変化をtoObservableでObservableに変換し、pipeのオペレーターで加工する例です。シグナルを書き換えるだけで値がラインに流れ込むため、既存のRxJS資産とシグナル中心の設計をつなげます。効果を確認するため、ここではログ出力だけの最小構成にしています。
import { Component, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { filter } from 'rxjs';
@Component({
selector: 'app-volume',
template:
'<button (click)="up()">音量アップ</button>' +
'<p>音量: {{ volume() }}</p>',
})
export class VolumeComponent {
volume = signal(0);
// シグナルの変化を Observable の流れに変換する
private volume$ = toObservable(this.volume);
constructor() {
// 8を超えたときだけ通す、という加工が RxJS でできる
this.volume$
.pipe(filter((v) => v > 8))
.subscribe((v) => console.log('音量が大きすぎます:', v));
}
up() {
this.volume.update((v) => v + 1);
}
}
4往復させる — シグナル入力からtoSignalの検索結果へ
検索語のシグナルをtoObservableで流れに変え、switchMapで通信し、結果をtoSignalでシグナルに戻す往復の例です。入力も結果もシグナルなのでテンプレートは完全にシグナルだけで書け、途中の非同期処理だけをRxJSに任せられます。シグナル時代の検索画面の実務形です。
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, switchMap } from 'rxjs';
type City = { id: number; name: string };
@Component({
selector: 'app-city-search',
template:
'<input (input)="onInput($event)" placeholder="都市名">' +
'@for (c of cities(); track c.id) { <p>{{ c.name }}</p> }',
})
export class CitySearchComponent {
private http = inject(HttpClient);
keyword = signal('');
// シグナル → Observable → 通信 → シグナル、と往復させる
cities = toSignal(
toObservable(this.keyword).pipe(
debounceTime(300),
switchMap((q) =>
this.http.get<City[]>('/api/cities', { params: { q } })
)
),
{ initialValue: [] as City[] }
);
onInput(e: Event) {
this.keyword.set((e.target as HTMLInputElement).value);
}
}
5intervalをtoSignalで受けて時計を作る
1秒ごとに値を発行するintervalをtoSignalでシグナルにし、経過秒数を表示する例です。時間とともに何度も届く値でも、テンプレート側は「今の値」を読むだけで自動的に更新されます。コンポーネント破棄時の購読解除もtoSignalが引き受けるため、解除コードは1行もありません。
import { Component } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { interval, map } from 'rxjs';
@Component({
selector: 'app-timer',
template: '<p>経過時間: {{ seconds() }} 秒</p>',
})
export class TimerComponent {
// 1秒ごとに届く値をシグナルとして受ける。解除は自動
seconds = toSignal(interval(1000).pipe(map((n) => n + 1)), {
initialValue: 0,
});
}