3
0

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のresource()とhttpResource()を試してみた

Posted at

Angular Advent Calendar 2025 の 7日目の記事です。

Angular では現在、実験的機能として非同期シグナルが提供されています。それが、
• resource()
• httpResource()

の2つです。

どちらも「非同期データをシグナル化して UI に反映する」という点では似ていますが、どのような違いがあるのか気になりました。
この記事では Qiita API を使って、それぞれの違いや使い分けをコード例とともに解説します。

🚀 この記事での目標

  • Resource / HttpResource の基本的な使い方
  • それぞれの挙動(自動リロード・パラメータ監視の仕組み)
  • 実際に使ってみて感じた「向き・不向き」
  • テスト方法の違い
  • 最終的な使い分け方針

Resource ― 任意の非同期処理を扱える万能シグナル

Angular には signal() や computed() といった同期シグナルがありますが、
resource() を使うと 非同期処理(Promise)もシグナルとして扱えるようになります。

特に便利なのは、

  • HTTP 結果を直接シグナルとしてUIに反映したいとき
  • パラメータ変更時に再取得したいとき

などです。

Resource の使い方

以下はユーザーIDに応じて Qiita の投稿一覧を取得する例です。

export class Qiita {
  userId = signal<string>(''); // ユーザIDのシグナル

  // ユーザIDに応じて URL を生成
  url = computed(() => {
    return this.userId() === ''
      ? `https://qiita.com/api/v2/items`
      : `https://qiita.com/api/v2/items?page=1&per_page=20&query=qiita+user%3A${this.userId()}`;
  });

  // Resource による非同期データ取得
  itemResource = resource({
    params: () => ({ url: this.url() }),   // URL の変更を監視対象にする
    loader: ({ params }) =>
      firstValueFrom(this.getItems(params.url)), // loader は Promise を返す必要あり
  });

  constructor(private httpClient: HttpClient) {}

  /**
   * Qiita API から投稿を取得
   */
  getItems(url: string) {
    return this.httpClient.get(url, {
      headers: { Authorization: 'Bearer ********' },
    })
    .pipe(
      map((json: any[]) =>
        json.map(row => ({
          id: row.id,
          title: row.title,
          userName: row.user.name,
        } satisfies Item))
      )
    );
  }
}

HTML 側(Resource の状態に応じて描画)

userId: <input type="text" [(ngModel)]="userId">

<section>
  <h2>Qiita Items from resource()</h2>
  @if (!itemResource.isLoading() && itemResource.hasValue()) {
    <ul>
      @for (item of itemResource.value(); track item) {
        <li>{{ item.title }} @if(item.userName) { by {{ item.userName }} }</li>
      }
    </ul>
    <button (click)="itemResource.reload()">Reload</button>
  } @else if (itemResource.error()) {
    <div>Error!!</div>
  } @else {
    <div>Loading...</div>
  }
</section>

Resource のポイントまとめ

  • params() の値が変わると loader() が再実行される
  • loader は Promise を返す必要がある
  • テンプレートでは value(), error(), isLoading() で状態を確認できる
  • HTTP 以外の非同期処理にも使える

params を使わないパターン

params を省略すると 値は自動更新されず、reload() のみで再取得になります。

itemResource = resource({
  loader: () => firstValueFrom(this.getItems(this.url())),
});

若干ハマりやすいポイントですが、この性質を活用すれば不要な自動更新を抑えることができます。

HttpResource ― HTTP 通信に特化した非同期シグナル

httpResource() は名前の通り HTTP 通信専用の Resource です。
内部で HttpClient をラップしており、以下のような特徴があります。

  • メソッド・URL・ヘッダなどをオプションで定義するだけで良い
  • parse だけ書けばレスポンスのマッピングができる
  • URL のシグナルが変わると自動で再取得

HttpResource の使い方

@Component({
  selector: 'app-qiita',
  imports: [FormsModule],
  templateUrl: './qiita.html',
})
export class Qiita {
  constructor(private httpClient: HttpClient) {}

  userId = signal('');
  url = computed(() => {
    return this.userId() === ''
      ? `https://qiita.com/api/v2/items`
      : `https://qiita.com/api/v2/items?page=1&per_page=20&query=qiita+user%3A${this.userId()}`;
  });

  itemHttpResource = httpResource<Item[]>(
    () => ({
      url: this.url(),
      method: 'GET',
      headers: { Authorization: 'Bearer ********' },
    }),
    {
      parse: (json: unknown) => {
        const rows = json as any[];
        return rows.map(row => ({
          id: row.id,
          title: row.title,
          userName: row.user.name,
        }));
      },
    }
  );
}

Resource と違い、loader や firstValueFrom を書く必要がありません。

HttpResource のテスト

HttpClient をモックすればテストできます。
resource() でも同様にテストできます。

describe('Qiita', () => {
  let component: Qiita;
  let fixture: ComponentFixture<Qiita>;
  let httpTestingController: HttpTestingController;
  const mockItems = [
    {
      id: 1,
      title: 'ほげほげ',
      user: {
        name: 'user1'
      }
    },
    {
      id: 2,
      title: 'ふがふが',
      user: {
        name: 'user2'
      }
    }
  ];
    beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [Qiita],
      providers: [
        provideHttpClient(),
        provideHttpClientTesting()
      ],
    })
    .compileComponents();
    
    fixture = TestBed.createComponent(Qiita);
    component = fixture.componentInstance;
    httpTestingController = TestBed.inject(HttpTestingController);
    });
    
    afterEach(() => {
    httpTestingController.verify();
    });
    
    it('should load items using httpResource() on initialization', async () => {
      fixture.detectChanges();
    
      const requests = httpTestingController.match(
        'https://qiita.com/api/v2/items'
      );
    
      requests.forEach(req => {
        expect(req.request.method).toBe('GET');
        req.flush(mockItems);
      });
    
      await fixture.whenStable();
      fixture.detectChanges();
    
      expect(component.itemHttpResource.hasValue()).toBe(true);
      expect(component.itemHttpResource.value()?.length).toBe(2);
    });
});

📝 まとめ:Resource と HttpResource の使い分け

両者を触ってみて、次の様な使い分けになるかなと感じました。

🔵 Resource が向くケース

  • 既存サービスの HTTP 呼び出しを「シグナル化」したい
  • すでに HttpClient を使ったサービス層がある
  • HTTP 以外の非同期処理(WebSocket、IndexedDB 等)で使いたい
  • 柔軟な前処理・後処理が必要

🟢 HttpResource が向くケース

  • 新しく HTTP 通信する機能を作成したい
  • 読み取り中心で、処理が単純
  • 「URL のシグナルが変わったら自動で再取得したい」

🎤 所感

Resource はとても柔軟で「好きな非同期処理をシグナルにできる強力な仕組み」だと感じました。
一方 HttpResource は HTTP 通信に必要なボイラープレートが大幅に減り、「小さく書きたい」ケースではかなり便利です。

どちらも非同期データを扱う UX を改善してくれるため、
状況に応じてうまく使い分けることで、より直感的でリアクティブなアプリケーションを構築しやすくなると思います。

3
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?