サンプルコードで身につける: 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[]>),
});
}