3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Angular】AIが書いてきた resource() って何?シグナルで非同期データを扱う新しいAPI

3
Posted at

【Angular】AIが書いてきた resource() って何?シグナルで非同期データを扱う新しいAPI

はじめに

AIにコードを書いてもらっていたら、見慣れない書き方が出てきました。

userResource = resource({
  params: () => ({ id: this.userId() }),
  loader: ({ params }) => fetchUser(params),
});

resource というシグナルらしき何かです。signalcomputed は見慣れていたのですが、これは初めて見ました。どういう仕組みで動いているのか気になったので、詳しく調べてみました。

環境

  • Angular 20
  • TypeScript 5.8

resource はまだ experimental(実験的)です
Angular 19 で導入されましたが、2026年3月時点でもまだ stable ではありません。今後 API が変わる可能性があるので、本番の重要な機能に使う場合はリスクを理解した上での判断が必要です。

そもそもなぜ resource が必要なのか

Angularのシグナル(signalcomputed など)はすべて同期的に動きます。値を読んだらその場で結果が返ってくる。

ところが実際のアプリではAPIからデータを取ってくる、つまり非同期な処理が必ず出てきます。computed の中で await は書けませんし、signal に非同期処理を詰め込もうとするとどこかで無理が出ます。

resource はその橋渡しをするAPIです。「シグナルの世界にいながら、非同期処理の結果を扱えるようにする」というのが目的です。

まず全体像を見る

resource の動きをまとめて確認できるサンプルです。ボタンでポケモンを切り替えると自動でデータを再取得し、ローディングやエラーも表示します。

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

interface Pokemon {
  id: number;
  name: string;
  height: number;
  weight: number;
  sprites: { front_default: string };
}

@Component({
  selector: 'app-pokemon',
  standalone: true,
  template: `
    <div>
      <button (click)="pokemonId.set(1)">フシギダネ</button>
      <button (click)="pokemonId.set(4)">ヒトカゲ</button>
      <button (click)="pokemonId.set(7)">ゼニガメ</button>
      <button (click)="pokemonResource.reload()">再取得</button>
    </div>

    <p>状態: {{ pokemonResource.status() }}</p>

    @if (pokemonResource.isLoading()) {
      <p>読み込み中...</p>
    } @else if (pokemonResource.error()) {
      <p>エラーが発生しました</p>
    } @else if (pokemonResource.hasValue()) {
      <img [src]="pokemonResource.value().sprites.front_default" />
      <p>名前: {{ pokemonResource.value().name }}</p>
      <p>高さ: {{ pokemonResource.value().height }}</p>
      <p>重さ: {{ pokemonResource.value().weight }}</p>
    }
  `,
})
export class PokemonComponent {
  pokemonId = signal(1);

  pokemonResource = resource<Pokemon, { id: number }>({
    params: () => ({ id: this.pokemonId() }),
    loader: ({ params, abortSignal }) =>
      fetch(`https://pokeapi.co/api/v2/pokemon/${params.id}`, {
        signal: abortSignal,
      }).then(res => res.json()),
  });
}

これで動く内容を踏まえつつ、各部分を掘り下げます。

params:「何を取りに行くか」を定義する

params: () => ({ id: this.pokemonId() }),

params にはリアクティブな計算式を書きます。computed と同じ仕組みで、内部で読んでいるシグナルの値が変わると自動で再計算されます。

pokemonId が変わる → params の値が変わる → loader が再実行される、という流れです。

request から params への改名について
Angular 19 では params ではなく request という名前でした。Angular 20 で params に改名されています。古い記事やサンプルコードを参照すると request が出てくることがあるので注意が必要です。

// Angular 19 までの書き方
pokemonResource = resource({
  request: () => ({ id: this.pokemonId() }),  // request
  loader: ({ request }) => fetch(`.../${request.id}`),
});

// Angular 20 以降
pokemonResource = resource({
  params: () => ({ id: this.pokemonId() }),   // params
  loader: ({ params }) => fetch(`.../${params.id}`),
});

paramsundefined を返すと loader は実行されず、状態が idle になります。

undefined を返す可能性がある場合は、型引数の第2ジェネリックに | undefined を追加する必要があります。また loader 内の params も型上は undefined になりうるため、! で非 null アサーションを付けます(loaderparamsundefined のときは実行されないので実際には安全です)。

// pokemonId が 0 のときは取りに行かない
pokemonResource = resource<Pokemon, { id: number } | undefined>({
  params: () => this.pokemonId() ? { id: this.pokemonId() } : undefined,
  loader: ({ params, abortSignal }) =>
    fetch(`https://pokeapi.co/api/v2/pokemon/${params!.id}`, {
      signal: abortSignal,
    }).then(res => res.json()),
});

「まだ取りに行く必要がない」というケースに使えます。

loader:非同期処理を書く

loader: ({ params, abortSignal }) =>
  fetch(`https://pokeapi.co/api/v2/pokemon/${params.id}`, {
    signal: abortSignal,
  }).then(res => res.json()),

loaderparams の値を受け取って非同期処理を行う関数です。async/await でも書けます。

loader: async ({ params, abortSignal }) => {
  const response = await fetch(
    `https://pokeapi.co/api/v2/pokemon/${params.id}`,
    { signal: abortSignal }
  );
  return response.json();
},

abortSignal でリクエストをキャンセルする

loader には abortSignal も渡ってきます。params が変わると、進行中の古いリクエストに対してキャンセル信号が送られます。fetchsignal オプションに渡しておくと、古いリクエストが自動でキャンセルされます。

たとえば検索ボックスに文字を打ちながら逐次検索するような場面で、前のリクエストが完了する前に次のリクエストを出した場合でも、前のレスポンスが後から返ってきて画面を上書きする、といった問題を防げます。

返ってくるシグナルたち

resource が返すオブジェクトには複数のシグナルが含まれています。

userResource.value()      // 取得した値(取得前は undefined)
userResource.isLoading()  // ロード中かどうか(boolean)
userResource.error()      // エラーがあれば格納される
userResource.status()     // 現在の状態(後述)
userResource.hasValue()   // 値があるかどうか(型ガードも兼ねる)

テンプレートでは isLoading()error() を使って状態を見ながら表示を切り替えます。非同期処理まわりの状態管理がシグナルだけで完結します。

hasValue() は型ガードを兼ねている

value() は取得前だと undefined を返します。また、エラー状態のときに value() を読むと例外が発生します(Angular 20 以降の挙動)。

hasValue() は TypeScript の型からも undefined を除いてくれるので、value() を安全に読む入口として使うのが推奨されています。

// これは型エラーになる可能性がある
const name = pokemonResource.value().name;

// hasValue() で確認してから読む
if (pokemonResource.hasValue()) {
  const name = pokemonResource.value().name; // ここでは undefined が除かれている
}

status で細かい状態を見る

status() は現在の状態を文字列で返します。isLoading() より細かく状態を把握したいときに使います。

status value() の中身 どんな状態か
'idle' undefined paramsundefined を返していてローダーが動いていない
'loading' undefined params が変わって初回ロード中
'reloading' 前回の値 reload() を呼んで再取得中(前の値は残っている)
'resolved' 取得した値 ロード成功
'error' undefined ロード失敗
'local' ローカルに設定した値 .set().update() で手動更新した

'reloading' のとき前回の値が残るのは実用的で、「再取得中も画面が真っ白にならない」という実装が自然に書けます。

status() の型は v20 で数値から文字列に変わりました
v19 では status() は数値を返していました(例:2 = Loading、4 = Resolved)。v20 以降は上記の文字列のユニオン型に変わっています。古いコードと見比べると数値と文字列が混在していて混乱しやすいので注意が必要です。

手動で再取得する

reload() を呼ぶと、params が変わっていなくても強制的に loader を再実行できます。

// ボタンを押したら再取得
onRefreshClick() {
  this.userResource.reload();
}

rxResource との違い

RxJS の Observable を返す処理を loader に書きたい場合は、rxResource が使えます。@angular/core/rxjs-interop からインポートします。

import { rxResource } from '@angular/core/rxjs-interop';
import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export class PokemonComponent {
  private http = inject(HttpClient);
  pokemonId = signal(1);

  pokemonResource = rxResource({
    params: () => ({ id: this.pokemonId() }),
    stream: ({ params }) =>
      this.http.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${params.id}`),
  });
}

API の形は resource とほぼ同じです。HttpClient を使っているプロジェクトではこちらの方が自然に書けます。

rxResourceloader は v20 で stream に改名されました
resourceloader はそのままですが、rxResource だけ v20 で名前が変わっています。古い記事やサンプルコードでは loader が使われていることがあるので注意が必要です。

// v19 の rxResource
rxResource({
  request: () => ({ id: this.pokemonId() }),   // request
  loader: ({ request }) =>                     // loader
    this.http.get(`.../${request.id}`),
});

// v20 以降の rxResource
rxResource({
  params: () => ({ id: this.pokemonId() }),    // params
  stream: ({ params }) =>                      // stream
    this.http.get(`.../${params.id}`),
});

従来の書き方との比較

resource が出る前は、こんな書き方をしていました。

// 従来の書き方(effect + signal)
pokemonId = signal(1);
pokemon = signal<Pokemon | undefined>(undefined);
isLoading = signal(false);
error = signal<unknown>(undefined);

constructor() {
  effect(() => {
    const id = this.pokemonId();
    this.isLoading.set(true);
    fetch(`https://pokeapi.co/api/v2/pokemon/${id}`)
      .then(res => res.json())
      .then(data => {
        this.pokemon.set(data);
        this.isLoading.set(false);
      })
      .catch(err => {
        this.error.set(err);
        this.isLoading.set(false);
      });
  });
}

ローディング状態やエラーを自前で管理する必要があり、コードが増えます。resource を使うと value()isLoading()error()status() が最初からついてくるので、この辺りを自前で書く必要がなくなります。

まとめ

  • resource はシグナルの世界で非同期処理を扱うための Angular 19 以降の API(experimental)
  • params は「何を取りに行くか」を定義するリアクティブな計算式。シグナルが変わると自動で loader が再実行される
  • loader は実際の非同期処理。params の値と abortSignal が渡ってくる
  • 返ってくる value()isLoading()error()status() で非同期状態管理が完結する
  • RxJS ベースで書きたいなら rxResource が使える

調べていて気になったのは、v19 → v20 の間だけでも requestparamsrxResourceloaderstreamstatus() の型が数値から文字列に変わるなど、破壊的な変更が立て続けに入っている点です。experimental である以上は今後も同様の変更が入る可能性があります。個人的には、本番の重要な機能への採用は安定版になるまで見送ろうと思っています。使ってみると確かに便利なので、早く stable になってほしいというのが正直なところです。

参考になったら いいねストック をお願いします!
同じような経験をされた方のコメントもお待ちしています。

参考

関連リンク

技術ブログでも学びや検証内容をまとめています。

nakamuuublog

アウトプットで手当がもらえる会社 ONE WEDGE

株式会社ONE WEDGE では一緒に働く仲間を募集中!

技術記事を書くと手当がもらえる「IT系記事寄稿特別手当」という制度があります。

興味があればぜひカジュアルに話しましょう!

👉 採用サイト

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?