Dev Study
← 解説「asyncパイプ — 購読と解除をAngularに任せる」に戻る

サンプルコードで身につける: asyncパイプ — 購読と解除をAngularに任せる

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1最小のasyncパイプ — 値をそのまま表示する

APIから取得した1件のデータを、subscribeを書かずにasyncパイプで直接表示する最小例です。コンポーネント側にはObservableのプロパティが1つあるだけで、状態への詰め替えコードがありません。購読も解除もAngularが自動で行います。

import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AsyncPipe } from '@angular/common';

type User = { id: number; name: string };

@Component({
  selector: 'app-profile',
  imports: [AsyncPipe],
  // async パイプが subscribe と解除を代行する
  template: '<p>ようこそ {{ (user$ | async)?.name }} さん</p>',
})
export class ProfileComponent {
  private http = inject(HttpClient);

  // $ 付きの名前は「Observable である」ことを示す慣習
  user$ = this.http.get<User>('/api/me');
}

2@if + as構文で「届いてから表示」する

@if (user$ | async; as user) と書くと、値が届くまでは@elseブロックが表示され、届いたら user という名前でブロック内から使えます。null チェックと読み込み中表示を1つの構文でまかなえる、asyncパイプの定番パターンです。

import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AsyncPipe } from '@angular/common';

type User = { id: number; name: string; email: string };

@Component({
  selector: 'app-account',
  imports: [AsyncPipe],
  // 届いた値に user という名前を付けてブロック内で使い回す
  template:
    '@if (user$ | async; as user) {' +
    '  <h2>{{ user.name }}</h2><p>{{ user.email }}</p>' +
    '} @else { <p>読み込み中...</p> }',
})
export class AccountComponent {
  private http = inject(HttpClient);
  user$ = this.http.get<User>('/api/me');
}

3@forと組み合わせて一覧を表示する

一覧データのObservableをasyncパイプで受け、@forでそのまま並べる例です。手動subscribeの版と比べると、signalへのsetもngOnInitも不要になっていることがわかります。一覧表示だけの画面なら、この形が最短の書き方です。

import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AsyncPipe } from '@angular/common';

type Article = { id: number; title: string };

@Component({
  selector: 'app-article-list',
  imports: [AsyncPipe],
  // as構文で1回だけ購読し、@for で並べる
  template:
    '@if (articles$ | async; as articles) {' +
    '  @for (a of articles; track a.id) { <p>{{ a.title }}</p> }' +
    '  @empty { <p>記事がありません</p> }' +
    '}',
})
export class ArticleListComponent {
  private http = inject(HttpClient);
  articles$ = this.http.get<Article[]>('/api/articles');
}

4手動subscribeとの比較 — 解除忘れが起きない

同じ画面を手動subscribeで書いた場合との比較です。手動版はngOnDestroyでの解除を忘れるとコンポーネント破棄後も購読が生き続けますが、asyncパイプ版にはそもそも解除するコードが存在しません。「書かなくていいコードはミスも起きない」ことを示す例です。

import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Subscription, interval } from 'rxjs';

// ── 手動 subscribe 版: 解除の管理が自分の責任になる ──
@Component({
  selector: 'app-clock-manual',
  template: '<p>経過秒数: {{ count() }}</p>',
})
export class ClockManualComponent implements OnInit, OnDestroy {
  count = signal(0);
  private sub?: Subscription;

  ngOnInit() {
    this.sub = interval(1000).subscribe((n) => this.count.set(n));
  }

  ngOnDestroy() {
    // これを忘れるとコンポーネント破棄後も購読が動き続ける
    this.sub?.unsubscribe();
  }
}

// ── async パイプ版: 解除のコード自体が存在しない ──
// @Component({
//   imports: [AsyncPipe],
//   template: '<p>経過秒数: {{ count$ | async }}</p>',
// })
// export class ClockComponent {
//   count$ = interval(1000);
// }

5pipeで加工したストリームをasyncパイプで受ける

コンポーネントではObservableをpipeで加工する定義だけを書き、表示はasyncパイプに任せる分業の例です。「加工はクラス側、購読はテンプレート側」という役割分担にすると、テストしやすく見通しの良い構造になります。

import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { AsyncPipe } from '@angular/common';
import { map } from 'rxjs';

type Task = { id: number; title: string; done: boolean };

@Component({
  selector: 'app-todo-summary',
  imports: [AsyncPipe],
  template: '<p>未完了タスク: {{ remaining$ | async }} 件</p>',
})
export class TodoSummaryComponent {
  private http = inject(HttpClient);

  // 加工(未完了の件数に変換)はクラス側、購読はテンプレート側
  remaining$ = this.http
    .get<Task[]>('/api/tasks')
    .pipe(map((tasks) => tasks.filter((t) => !t.done).length));
}