Dev Study
← 解説「resource() — 非同期データをシグナルで扱う」に戻る

サンプルコードで身につける: resource() — 非同期データをシグナルで扱う

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

1最小の resource()

resource() は params と loader の2つを渡して作ります。loader に非同期取得の処理を書くと、結果が value() というシグナルとして手に入ります。ngOnInit で subscribe して状態へ詰め替える、というこれまでの定型コードがまるごと不要になるのが出発点です。

import { Component, resource } from '@angular/core';

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

@Component({
  selector: 'app-article-list',
  // 結果はシグナルなので、テンプレートで value() を読むだけ
  template:
    '@for (a of articles.value() ?? []; track a.id) {' +
    '  <p>{{ a.title }}</p>' +
    '}',
})
export class ArticleListComponent {
  // loader に非同期取得を書くと、結果が value() で読めるようになる
  articles = resource({
    params: () => ({}),
    loader: () =>
      fetch('/api/articles').then((r) => r.json() as Promise<Article[]>),
  });
}

2params のシグナルが変わると自動で再取得

params には依存するシグナルを読む関数を渡します。ここでは userId() を読んでいるため、userId.set() で値を変えるたびに loader が自動で再実行され、最新の1件を取り直します。進行中の古いリクエストは中断(abort)されるので、購読の管理を自分で書く必要がありません。

import { Component, signal, resource } from '@angular/core';

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

@Component({
  selector: 'app-user-switcher',
  template:
    '<button (click)="userId.set(userId() + 1)">次のユーザー</button>' +
    '<p>{{ userResource.value()?.name }}</p>',
})
export class UserSwitcherComponent {
  userId = signal(1);

  // params が userId を読んでいるので、userId が変わるたびに再取得される
  userResource = resource({
    params: () => ({ id: this.userId() }),
    loader: ({ params }) =>
      fetch(`/api/users/${params.id}`).then((r) => r.json() as Promise<User>),
  });
}

3value / isLoading / error の3つの状態

resource は取得結果だけでなく、状態もシグナルとして持ちます。value() が取得した値、isLoading() が読み込み中かどうか、error() がエラーで、さらに status() で 'idle' / 'loading' / 'resolved' / 'error' といった状態の文字列も読めます。非同期処理に必ず付いてくる3つの状態が、最初から揃っているのが便利な点です。

import { Component, signal, resource, effect } from '@angular/core';

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

@Component({
  selector: 'app-product-detail',
  template: '<p>{{ productResource.value()?.name }}</p>',
})
export class ProductDetailComponent {
  productId = signal(1);

  productResource = resource({
    params: () => ({ id: this.productId() }),
    loader: ({ params }) =>
      fetch(`/api/products/${params.id}`).then((r) => r.json() as Promise<Product>),
  });

  constructor() {
    effect(() => {
      // 値・読み込み中・エラー・状態文字列、すべてシグナルとして読める
      console.log('値:', this.productResource.value());
      console.log('読み込み中?:', this.productResource.isLoading());
      console.log('エラー:', this.productResource.error());
      console.log('状態:', this.productResource.status());
    });
  }
}

4テンプレートで読み込み中・成功・失敗を分岐表示する

状態がすべてシグナルなので、テンプレートの @if でそのまま表示を出し分けられます。isLoading() ならスピナー、error() ならエラーメッセージ、それ以外は取得した値、という3分岐が定番の形です。非同期画面の骨組みとして、このパターンをそのまま覚えてしまうのが実務的です。

import { Component, signal, resource } from '@angular/core';

type Weather = { city: string; temperature: number };

@Component({
  selector: 'app-weather',
  // 読み込み中 → 失敗 → 成功、の順に @if で出し分ける
  template:
    '@if (weatherResource.isLoading()) {' +
    '  <p>読み込み中...</p>' +
    '} @else if (weatherResource.error()) {' +
    '  <p>天気の取得に失敗しました</p>' +
    '} @else {' +
    '  <p>{{ weatherResource.value()?.city }}: {{ weatherResource.value()?.temperature }}度</p>' +
    '}',
})
export class WeatherComponent {
  city = signal('tokyo');

  weatherResource = resource({
    params: () => ({ city: this.city() }),
    loader: ({ params }) =>
      fetch(`/api/weather/${params.city}`).then((r) => r.json() as Promise<Weather>),
  });
}

5reload() で同じ条件のまま取り直す

params は変わっていないが最新のデータを取り直したい、というときは reload() を呼びます。「更新ボタン」や「エラー後の再試行」がこの1メソッドで実現でき、再取得中は isLoading() が再び true になります。依存シグナルの変更による自動再取得と、reload() による手動の取り直し、の2本立てで使い分けます。

import { Component, resource } from '@angular/core';

type Notice = { id: number; text: string };

@Component({
  selector: 'app-notice-board',
  // ボタンで reload() を呼ぶと、同じ条件で loader が再実行される
  template:
    '<button (click)="noticeResource.reload()">最新に更新</button>' +
    '@if (noticeResource.isLoading()) {' +
    '  <p>更新中...</p>' +
    '} @else {' +
    '  @for (n of noticeResource.value() ?? []; track n.id) { <p>{{ n.text }}</p> }' +
    '}',
})
export class NoticeBoardComponent {
  noticeResource = resource({
    params: () => ({}),
    loader: () =>
      fetch('/api/notices').then((r) => r.json() as Promise<Notice[]>),
  });
}