Dev Study
← 解説「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,
  });
}