この記事ではAngular v19で新たに追加されるSignal関連の実験的API resource()
について解説する。なお、書いている時点で最新の v19.0.0-next.11
をベースにしているため、正式リリースまでに変更される可能性はある。また、そもそも実験的APIなのでリリース後にも変更されている可能性はあることに注意してほしい。
Angular v19についての他の記事
resource()
とは何か?
resource()
は、非同期的に読み込まれるデータをシグナルとして扱えるようにするAPIである。
具体的なコードを見ればわかりやすい。次のコードは、resource()
を使ってHTTP通信を行い、サーバーAPIから製品情報を取得している。リクエストのパラメータには親コンポーネントから受け取ったproductId
が使われている。productId
の値が変更されたらデータの再取得を行う。非同期データの値と取得状態がカプセル化されているのがResourceRef
型のproductResource
フィールドである。
@Component({
selector: 'app-product-viewer',
template: `
@if (productResource.value(); as product) {
<p> Title: {{ product.title }} </p>
} @else if (productResource.error(); ) {
<p> load failed </p>
} @else if(productResource.isLoading()) {
<p> loading... </p>
}
`,
})
export class ProductViewer {
productId = input.required<number>();
productResource: ResourceRef<Product> = resource({
request: () => this.productId(), // load on productId change
loader: async ({ request: productId, abortSignal }) => {
const resp = await fetch(`https://dummyjson.com/products/${productId}`, {
signal: abortSignal,
});
return resp.json() as Promise<Product>;
},
});
}
resource()
が引数に取るオブジェクトの中身を詳しく見てみよう。
request
プロパティは、非同期データの取得をトリガーするリクエストパラメータを返す関数である。この関数はcomputed()
と同等に、他のシグナルのgetterを呼び出すことで派生値を生み出し、依存したシグナルの更新により再計算される。つまり、上述の例では、this.productId()
の値が更新されるたびにリクエストも更新され、データ取得がトリガーされる。
loader
プロパティはデータの取得手続きを記述する関数である。request
プロパティの関数が返した値を引数から取り出して、実際のデータ読み込みに利用できる。最終的にこの関数が返したオブジェクトはResourceRef.value()
シグナルに格納されることになる。返り値がPromiseであれば解決後の中身だけがシグナルに格納されるため、Signal<Promise<T>>
にはならない。
loader
関数の引数にはabortSignal
も含まれており、Angular側のスケジューリングやライフサイクル管理によってコンポーネントが破棄されるときには進行中のリクエストも中断できるように、データ取得ロジックの中で利用できる。
これがresource()
の基本的なインターフェースと使い方である。ここで例に挙げたのはFetch APIを使ったHTTP通信によるデータ取得だが、パラメータを引数にとってT
またはPromise<T>
で値を返すインターフェースに合致するならば、どのようなデータソースでもいいし、どのような取得方法でもよい。Local StorageやIndexedDBへのアクセスをラップしてもよいし、Web Workerを使って別スレッドで計算した結果を取得するというのもありえるだろう。
従来はこのようなユースケースはsignal()
とeffect()
によって解決されていたが、副作用として何でもできてしまうeffect()
を使わずに済み、なおかつ意図が明確なresource()
ひとつで完結するのは嬉しい改善だ。上述の例をresource()
なしでやろうとすると次のようになるが、やることに対してコードが多く複雑すぎる。
// resource() がない場合
export class ProductViewer {
productId = input.required<number>();
productData = signal<Product | null>(null);
isProductLoading = signal<boolean>(false);
constructor() {
effect(async (onCleanup) => {
const productId = this.productId();
this.isProductLoading.set(true);
const abortCtrl = new AbortController();
onCleanup(() => abortCtrl.abort())
const resp = await fetch(`https://dummyjson.com/products/${productId}`, {
signal: abortCtrl.signal,
});
const data = await resp.json() as Promise<Product>;
this.productData.set(data);
this.isProductLoading.set(false);
});
}
}
HttpClient
とrxResource
ところで、Angularで非同期データの取得といえばHttpClient
APIが代表的な機能だが、ここまでの例に登場していない。上述のサンプルコードでは意図的にWeb標準のFetch APIを使っている。
なぜかというと、resource()
のloader
関数はPromiseからシグナルへの変換を行うが、Obervable
型の値からシグナルへの変換をしないからだ。HttpClientのメソッドが返す値はObservable
なので、リターンする前に自前で変換する必要がある。RxJSが提供しているfirstValueFrom
関数を使えば変換はできるが、resource()
というAngularのコアAPI(候補)の中で、Observable
は第一級サポートされないインターフェースである。
export class ProductViewer {
productId = input.required<number>();
http = inject(HttpClient);
productResource: ResourceRef<Product> = resource({
request: () => this.productId(), // load on productId change
loader: ({ request: productId, abortSignal }) => {
const destroy$ = fromEvent(abortSignal, "abort");
return firstValueFrom(
this.http.get<Product>(`https://dummyjson.com/products/${productId}`)
.pipe(takeUntil(destroy$))
);
},
});
}
とはいえ実際には多くのアプリケーションでHttpClient
が使われており、resource()
との併用が望まれるのも当然わかりきっているので、RxJSとの相互運用性のためのサブパッケージ @angular/core/rxjs-interop
からrxResource()
というAPIも提供される。これはresource()
とほぼ同じインターフェースを持っているが、loader
関数がObservable
型にも対応している。次のサンプルコードのように、HttpClient
のメソッドの戻り値を返すだけで、コンポーネントの破棄によるリクエストの中断も含めてすべてやってくれる。
export class ProductViewer {
productId = input.required<number>();
http = inject(HttpClient);
productResource: ResourceRef<Product> = rxResource({
request: () => this.productId(), // load on productId change
loader: ({ request: productId }) => {
return this.http.get<Product>(`https://dummyjson.com/products/${productId}`);
},
});
}
AngularのフレームワークコアからだんだんとObservable
の第一級サポートが消えていっているが、一方でHTTPクライアントやフォームAPIなどにはまだまだObservable
ベースのAPIが残っている。それらがまだ必要な間は無理にresource()
のようなコアAPIにこだわらなくても、rxResource()
などの相互運用性パッケージを利用して何も問題ないだろう。待っていれば公式にObservable
非依存のHTTPクライアントも来るだろうから、そのときに乗り換えればいい。
まとめ
-
resource()
は、Angular v19で導入される新しいSignal関連の実験的APIである。 - 非同期データの取得と管理を簡潔に行うことができ、従来の
signal()
とeffect()
の組み合わせよりも意図が明確になる。 -
request
とloader
関数を指定することで、データの取得条件とロジックを定義できる。 - Promiseベースのインターフェースで、Fetch API、IndexedDB、Web Workerなど、様々なデータソースに対応可能。
-
HttpClient
との併用にはrxResource()
が提供され、Observable
との相互運用性を確保している。 - 将来的に
Observable
非依存のHTTPクライアントが登場する可能性があるが、それまでは相互運用性パッケージを利用することで問題なく開発を進められる。
今回のサンプルコードもStackblitzに置いているので好きに使ってほしい。