Angular の httpResource とは、signal に連動して HTTP の GET を自動で実行し、結果・読み込み中・エラーを signal で返す API です。 汎用版の resource(Promise を返す関数で読み込む)と、RxJS 版の rxResource(Observable を返す関数で読み込む)があり、3つとも Angular v22 で安定版(型定義で @publicApi 22.0)になりました。 v19〜v21 の実験的 API のころとは、request → params、loader → stream(rxResource)など名前が変わっている点に注意が必要です。

この記事では Angular 22.2.0 で、ローカルのモック API を相手に 一覧取得・ID 切り替え(前のリクエストのキャンセル)・404 エラー表示・chain() による依存リクエスト・set/update と reload・zod での検証を実際に動かし、Playwright で状態の移り変わりを記録しました。HttpClient+subscribe からの書き換え前後、provideHttpClientTesting を使った Vitest のテストと実行結果、テストでつまずいた点も載せています。

httpResource / resource の早見表

やりたいこと 書き方 補足
GET で JSON を取得 httpResource<Task[]>(() => '/api/tasks') @angular/common/http から import
初期値を空配列にする httpResource(() => url, { defaultValue: [] }) 読み込み中・idle の間も value() が []
signal の変化で取り直す httpResource(() => '/api/tasks/' + id()) 前のリクエストは自動でキャンセル
条件がそろうまで送らない 関数から undefined を返す 状態は idle
クエリ・ヘッダー付き httpResource(() => ({ url, params: { q: kw() }, headers: { ... } })) オブジェクト形式
レスポンスを検証・変換 { parse: TaskListSchema.parse } zod などの関数をそのまま渡せる
文字列・Blob で受け取る httpResource.text() / .blob() / .arrayBuffer() 既定は JSON
前のリソースの結果を使う httpResource(({ chain }) => '/api/users/' + chain(task)?.ownerId) 上流の loading / error が伝わる
取り直す res.reload() 状態は reloading、値は保持
手元で書き換える res.set(v) / res.update(fn) 状態は local
fetch など Promise で読む resource({ params, loader }) abortSignal を fetch に渡すとキャンセル可
Observable で読む rxResource({ params, stream }) @angular/core/rxjs-interop から import
入力が止まってから検索 debounced(() => kw(), 300) + chain() v22 で experimental
テストでレスポンスを返す provideHttpClientTesting() + HttpTestingController expectOne().flush()

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angular httpResource使い方|v22安定版の実例とテスト」

resource・rxResource・httpResource の使い分け

API import 元 読み込み関数 向いている場面
httpResource @angular/common/http URL かリクエストオブジェクトを返す関数 HTTP の GET で読むだけ。インターセプターやテスト用バックエンドもそのまま使える
rxResource @angular/core/rxjs-interop stream: ({ params }) => Observable Observable を返す既存サービスを使う、retry などの演算子を足したい
resource @angular/core loader: ({ params, abortSignal }) => Promise fetch・IndexedDB・Web Worker など HTTP 以外の非同期処理

3つとも戻り値は同じ ResourceRef 系で、状態の読み方(value・status・error・isLoading・hasValue)は共通です。 POST・PUT などの更新処理は、公式ドキュメントでも httpResource ではなく HttpClient を直接使うよう案内されています。

共通のプロパティ・メソッド

名前 型 内容
value() signal 最新の値。error 状態で読むと例外を投げる
hasValue() メソッド 値があるか。true なら型から undefined が外れる
status() signal 6つの状態(下表)
error() signal error 状態のときのエラー
isLoading() signal loading か reloading なら true
snapshot() signal 状態と値(またはエラー)をまとめたオブジェクト
reload() メソッド 同じ条件で取り直す
set() / update() メソッド 手元で値を書き換える
destroy() メソッド 破棄し、実行中のリクエストをキャンセル
headers() / statusCode() / progress() signal httpResource のみ。レスポンスヘッダー、HTTP ステータス、進捗

status の6つの値

status いつ value()
idle params(URL)が undefined で、何もしていない defaultValue(なければ undefined)
loading params が変わって読み込み中 defaultValue(前の値は消える)
reloading reload() で読み込み中 前の値を保持
resolved 読み込み完了 結果
local set() / update() で書き換えた 書き換えた値
error 読み込み失敗 読むと例外。error() を見る

検証環境とモック API

項目 バージョン
Angular(core / common / cli) 22.2.0(zoneless 既定)
zod 4.6.5
Vitest 5.0.2(ng test)
Node.js 24.21.0
Playwright / Chromium 1.56.1 / 141

プロジェクトは npx -y -p @angular/cli@22 ng new res-app --defaults --skip-git --style=css --routing=false --ssr=false --ai-config=none で作成し、npm i zod を追加しました。モック API はビルド成果物の配信も兼ねた Node の小さなサーバーで、?delay=ミリ秒 で応答を遅らせ、途中で接続が切られたら ABORTED と記録します。

// server.mjs(node server.mjs dist/res-app/browser 7123)
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';

const [dist, port] = [process.argv[2], Number(process.argv[3] ?? 7120)];
const tasks = [
  { id: 1, title: '備品リストを更新する', done: false, ownerId: 10 },
  { id: 2, title: '会議室を予約する', done: true, ownerId: 11 },
  { id: 3, title: '週報を書く', done: false, ownerId: 10 },
];
const users = { 10: { id: 10, name: '佐藤' }, 11: { id: 11, name: '鈴木' } };
const types = { '.js': 'text/javascript', '.css': 'text/css', '.html': 'text/html' };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

http.createServer(async (req, res) => {
  const url = new URL(req.url, 'http://x');
  const json = (code, body) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(JSON.stringify(body)); };
  if (url.pathname.startsWith('/api/')) {
    console.log(new Date().toISOString().slice(11, 23), 'REQ', url.pathname + url.search);
    let closed = false;
    req.on('close', () => { if (!res.writableEnded) { closed = true; console.log(new Date().toISOString().slice(11, 23), 'ABORTED', url.pathname); } });
    await sleep(Number(url.searchParams.get('delay') ?? 300));
    if (closed) return;
    let m;
    if (url.pathname === '/api/tasks') return json(200, tasks);
    if ((m = url.pathname.match(/^\/api\/tasks\/(\d+)$/))) {
      const t = tasks.find((x) => x.id === Number(m[1]));
      return t ? json(200, t) : json(404, { message: `task ${m[1]} not found` });
    }
    if ((m = url.pathname.match(/^\/api\/users\/(\d+)$/))) return users[m[1]] ? json(200, users[m[1]]) : json(404, {});
    if (url.pathname === '/api/broken') return json(200, [{ id: 'x', title: 42 }]); // 型が違うデータ
    return json(404, { message: 'not found' });
  }
  let file = path.join(dist, url.pathname === '/' ? 'index.html' : url.pathname);
  if (!fs.existsSync(file) || fs.statSync(file).isDirectory()) file = path.join(dist, 'index.html');
  res.writeHead(200, { 'content-type': types[path.extname(file)] ?? 'application/octet-stream' });
  fs.createReadStream(file).pipe(res);
}).listen(port, () => console.log('listening', port));

v21 以降は HttpClient が既定で注入できるため、今回の app.config.ts には provideHttpClient() を書いていません(それでも動きました)。 インターセプター(withInterceptors)や withFetch() などを設定する場合は、これまでどおり provideHttpClient(...) を書きます。

書き換え前:HttpClient+subscribe

ID を切り替えて詳細を取る処理を、signal と RxJS で書いた例です(ビルドが通ることを確認済み)。

import { Component, DestroyRef, OnInit, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { Subject, switchMap } from 'rxjs';
import { Task } from './task.model';

@Component({
  selector: 'app-task-detail-before',
  template: `
    @if (loading()) { 読み込み中... }
    @else if (error()) { エラー: {{ error() }} }
    @else if (task(); as t) { {{ t.title }} }
  `,
})
export class TaskDetailBefore implements OnInit {
  private http = inject(HttpClient);
  private destroyRef = inject(DestroyRef);
  private id$ = new Subject<number>();

  task = signal<Task | undefined>(undefined);
  loading = signal(false);
  error = signal<string | undefined>(undefined);

  ngOnInit() {
    this.id$
      .pipe(
        switchMap((id) => {           // 前のリクエストをキャンセルするため switchMap
          this.loading.set(true);
          this.error.set(undefined);
          return this.http.get<Task>(`/api/tasks/${id}`);
        }),
        takeUntilDestroyed(this.destroyRef),
      )
      .subscribe({
        next: (t) => { this.task.set(t); this.loading.set(false); },
        error: (e) => { this.error.set(e.message); this.loading.set(false); }, // エラー後は購読が終わる
      });
  }

  select(id: number) {
    this.id$.next(id);
  }
}

読み込み中・エラー・値の3つの signal、Subject、switchMap、購読解除を自分で管理する必要があります。 また、この書き方では一度エラーになると購読が終わり、次に ID を選んでも取得されません(catchError を switchMap の内側に入れる工夫が必要です)。

書き換え後:httpResource(コピペで動く完成形)

// src/app/task.model.ts
import { z } from 'zod';

export const TaskSchema = z.object({
  id: z.number(),
  title: z.string(),
  done: z.boolean(),
  ownerId: z.number(),
});
export type Task = z.infer<typeof TaskSchema>;
export const TaskListSchema = z.array(TaskSchema);

export interface User { id: number; name: string }
// src/app/app.ts
import { Component, computed, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { Task, TaskListSchema, User } from './task.model';

@Component({
  selector: 'app-root',
  template: `
    <h2>タスク一覧 <small data-testid="list-status">[{{ tasks.status() }}]</small></h2>
    <button (click)="tasks.reload()">再読み込み</button>
    <ul>
      @for (t of tasks.value(); track t.id) {
        <li>
          <button (click)="selectedId.set(t.id)">#{{ t.id }}</button>
          {{ t.title }} {{ t.done ? '(完了)' : '' }}
          <button (click)="toggleLocal(t.id)">完了切替(ローカル)</button>
        </li>
      }
    </ul>
    <button (click)="selectedId.set(99)">#99(存在しない)</button>

    <h2>詳細 <small data-testid="detail-status">[{{ task.status() }}]</small></h2>
    <div data-testid="detail">
      @if (task.hasValue()) {
        {{ task.value().title }}(担当: {{ owner.hasValue() ? owner.value().name : '…' }})
      } @else if (task.error(); as err) {
        エラー: {{ task.statusCode() }} {{ err.message }}
      } @else if (task.isLoading()) {
        読み込み中...
      }
    </div>
    <p data-testid="owner-status">担当者リソース: [{{ owner.status() }}]</p>

    <h2>スキーマ検証</h2>
    <p data-testid="broken">{{ broken.error()?.message ?? broken.status() }}</p>
  `,
})
export class App {
  // 1) 一覧: defaultValue で「空配列」から始める
  readonly tasks = httpResource(() => '/api/tasks', {
    parse: TaskListSchema.parse,
    defaultValue: [],
  });

  // 2) 詳細: selectedId が変わるたびに自動で再取得(前のリクエストはキャンセル)
  readonly selectedId = signal<number | undefined>(undefined);
  readonly task = httpResource<Task>(() => {
    const id = this.selectedId();
    return id === undefined ? undefined : `/api/tasks/${id}?delay=800`;
  });

  // 3) 依存リクエスト: task が resolved になってから担当者を取得
  readonly owner = httpResource<User>(({ chain }) => {
    const task = chain(this.task); // task が resolved でなければ、ここで状態が伝わる
    return task ? `/api/users/${task.ownerId}` : undefined;
  });

  // 4) parse でレスポンスを検証(壊れたデータを返す API)
  readonly broken = httpResource(() => '/api/broken', { parse: TaskListSchema.parse });

  readonly doneCount = computed(() => this.tasks.value().filter((t) => t.done).length);

  toggleLocal(id: number) {
    this.tasks.update((list) => list.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
  }
}

@if (task.hasValue()) の中では task.value() の型から undefined が外れるので、task.value().title とそのまま書けます。hasValue() は error 状態で value() を読んで例外になるのを防ぐ役目もあります。

実行結果:状態の移り変わり(Playwright)

本番ビルドをモックサーバーで配信し、Playwright で画面の表示とネットワークを記録しました。

  182ms  一覧[loading] 詳細[idle] 「」 担当者リソース: [idle]
  804ms  一覧[resolved] 詳細[idle] 「」 担当者リソース: [idle]
  804ms  --- #1 → #2 → #3 を 100ms 間隔でクリック
  973ms  requestfailed /api/tasks/1 net::ERR_ABORTED
 1106ms  requestfailed /api/tasks/2 net::ERR_ABORTED
 1201ms  一覧[resolved] 詳細[loading] 「読み込み中...」 担当者リソース: [loading]
 2111ms  一覧[resolved] 詳細[resolved] 「週報を書く(担当: …)」 担当者リソース: [loading]
 2525ms  一覧[resolved] 詳細[resolved] 「週報を書く(担当: 佐藤)」 担当者リソース: [resolved]
 2525ms  --- #99
 3559ms  一覧[resolved] 詳細[error] 「エラー: 404 Http failure response for http://localhost:7123/api/tasks/99?delay=800: 404 Not Found」 担当者リソース: [error]
 3559ms  --- 完了切替(ローカル) を1件目で
 3653ms  一覧[local] ... / 1件目: #1 備品リストを更新する (完了) 完了切替(ローカル)
 3653ms  --- 再読み込み
 3730ms  一覧[reloading] ...
 4245ms  一覧[resolved] ... / 1件目: #1 備品リストを更新する 完了切替(ローカル)

同じ時間帯のモックサーバーのログです。

03:41:26.151 REQ /api/tasks/1?delay=800
03:41:26.275 REQ /api/tasks/2?delay=800
03:41:26.279 ABORTED /api/tasks/1
03:41:26.409 ABORTED /api/tasks/2
03:41:26.409 REQ /api/tasks/3?delay=800
03:41:27.218 REQ /api/users/10
03:41:27.859 REQ /api/tasks/99?delay=800
03:41:28.990 REQ /api/tasks

結果から読み取れることを表にまとめます。

確認したこと 結果
ID を素早く切り替えたとき #1・#2 のリクエストはブラウザ側でもサーバー側でも中断(ABORTED)され、最後の #3 だけが完了
chain() の依存リクエスト task が loading の間は owner も loading。task が resolved になった直後に /api/users/10 を送信
404 のとき task は error、statusCode() は 404。chain している owner にも error が伝わった
update() で書き換え 状態が local になり、画面に「(完了)」が付く(サーバーには送らない)
reload() reloading の間も一覧は表示されたまま。完了後はサーバーの値(未完了)に戻る

switchMap・catchError・loading フラグを書かずに、キャンセルとエラー処理、依存関係の状態伝播まで揃うのが httpResource の強みです。書き換え前の subscribe 版とは違い、404 のあとで別の ID を選べば、そのまま再取得されます。

parse でレスポンスを検証する(zod)

/api/broken は [{ id: 'x', title: 42 }] という型の合わないデータを返します。parse: TaskListSchema.parse を渡した broken リソースの error().message は次のとおりでした(先頭のみ)。

[ { "expected": "number", "code": "invalid_type", "path": [ 0, "id" ], "message": "Invalid input: expected number, received string" }, { "expected": "string", "code": "invalid_type", "path": [ 0, "title" ], "message": "Invalid input: expected string, received number" }, ...

parse が例外を投げると、リソースは error 状態になります。 型の不一致を画面の奥で undefined エラーとして踏むのではなく、取得した時点で気づけるのが利点です。parse の戻り値の型がそのまま value() の型になるので、ジェネリクス(httpResource<Task[]>)を書く必要もありません。

一方でバンドルサイズは増えます。今回の構成で ng build の Initial total を比べると、zod なし 153.06kB、zod 使用 607.41kB、zod/mini 使用 580.76kB でした(zod/mini は parse: (v) => z.parse(TaskListSchema, v) と書きます)。検証を入れる画面を限定する、手書きの型チェック関数を parse に渡す、といった選択肢も検討してください。

resource と rxResource の書き方

HTTP 以外や既存の Observable を使う場合の例です(上の画面に組み込んで動作確認しました)。

// src/app/extras.ts
import { Component, inject, resource, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs';
import { Task } from './task.model';

@Component({
  selector: 'app-extras',
  template: `
    <input data-testid="kw" (input)="keyword.set($any($event.target).value)" placeholder="キーワード" />
    <p data-testid="fetch">resource: [{{ byFetch.status() }}] {{ byFetch.value()?.join(', ') }}</p>
    <p data-testid="rx">rxResource: [{{ byRx.status() }}] {{ byRx.value() }}件</p>
  `,
})
export class Extras {
  keyword = signal('');

  // resource + fetch: abortSignal を渡すと、キーワード変更時に前の fetch が中断される
  byFetch = resource({
    params: () => ({ q: this.keyword() }),
    loader: async ({ params, abortSignal }) => {
      const res = await fetch(`/api/tasks?delay=500&q=${encodeURIComponent(params.q)}`, { signal: abortSignal });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      const list: Task[] = await res.json();
      return list.filter((t) => t.title.includes(params.q)).map((t) => t.title);
    },
  });

  // rxResource: Observable を返す stream を使う
  private http = inject(HttpClient);
  byRx = rxResource({
    params: () => this.keyword(),
    stream: ({ params }) =>
      this.http.get<Task[]>('/api/tasks').pipe(map((list) => list.filter((t) => t.title.includes(params)).length)),
    defaultValue: 0,
  });
}

「備品」と1文字ずつ(100ms 間隔)入力したときの記録です。

  927ms  resource: [resolved] 備品リストを更新する, 会議室を予約する, 週報を書く | rxResource: [resolved] 3件
  927ms  --- 「備品」と1文字ずつ入力(100ms間隔)
 1145ms  requestfailed ?delay=500&q=%E5%82%99 net::ERR_ABORTED
 1145ms  requestfailed  net::ERR_ABORTED
 1146ms  resource: [loading] | rxResource: [loading] 0件
 1851ms  resource: [resolved] 備品リストを更新する | rxResource: [resolved] 1件

1文字目(「備」)のリクエストは、resource(fetch)も rxResource(HttpClient)も2文字目の入力で中断されました。 resource では abortSignal を fetch に渡さないと中断されない点に注意してください。また、loading の間 rxResource の値は前の「3件」ではなく defaultValue の 0 に戻っています。前の値を表示し続けたい場合は、linkedSignal などで直前の値を別に持つ必要があります。

rxResource の stream が値を1つも出さずに完了すると(例:catchError(() => EMPTY))、型定義のコメントによると NG0991 のエラーになります。エラーは値に変えるか、そのまま流して error() で受け取るのがよいでしょう。

入力が止まってから検索する:debounced(experimental)

v22 では debounced()(@angular/core、experimental)が追加されました。戻り値がリソースなので、chain() と組み合わせられます。

import { Component, debounced, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';

@Component({
  selector: 'app-task-search',
  template: `<input (input)="keyword.set($any($event.target).value)" />
             <p>[{{ result.status() }}] {{ result.value() }}</p>`,
})
export class TaskSearch {
  keyword = signal('');
  q = debounced(() => this.keyword(), 300);                             // 300ms 入力が止まったら確定
  result = httpResource(({ chain }) => `/api/tasks?q=${chain(this.q)}`); // 確定するまで loading
}

Vitest で「備 → 備品」と 100ms 間隔で入力し、その後 300ms 進めるテストを実行したところ、送信されたリクエストは /api/tasks?q=備品 の1本だけで、待っている間の result.status() は loading でした。experimental のため、今後 API が変わる可能性があります。

Vitest でテストする(provideHttpClientTesting)

// src/app/app.spec.ts
import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { App } from './app';

describe('App(httpResource)', () => {
  let http: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [App],
      providers: [provideHttpClient(), provideHttpClientTesting()],
    });
    http = TestBed.inject(HttpTestingController);
  });

  afterEach(() => http.verify());

  function setup() {
    const fixture = TestBed.createComponent(App);
    TestBed.tick(); // httpResource がリクエストを出す
    http.expectOne('/api/broken').flush([]);
    return { fixture, app: fixture.componentInstance };
  }

  it('一覧を取得して表示する', async () => {
    const { fixture, app } = setup();
    expect(app.tasks.status()).toBe('loading');
    expect(app.tasks.value()).toEqual([]); // defaultValue

    http.expectOne('/api/tasks').flush([
      { id: 1, title: '備品リストを更新する', done: false, ownerId: 10 },
    ]);
    await fixture.whenStable();
    expect(app.tasks.status()).toBe('resolved');
    expect(fixture.nativeElement.textContent).toContain('備品リストを更新する');
  });

  it('ID を切り替えると前のリクエストはキャンセルされる', async () => {
    const { fixture, app } = setup();
    http.expectOne('/api/tasks').flush([]);

    app.selectedId.set(1);
    TestBed.tick();
    const first = http.expectOne('/api/tasks/1?delay=800');

    app.selectedId.set(2);
    TestBed.tick();
    expect(first.cancelled).toBe(true);

    http.expectOne('/api/tasks/2?delay=800')
      .flush({ id: 2, title: '会議室を予約する', done: true, ownerId: 11 });
    await Promise.resolve(); // レスポンスがリソースに反映されるのを待つ(マイクロタスク1回)
    TestBed.tick();
    http.expectOne('/api/users/11').flush({ id: 11, name: '鈴木' });
    await fixture.whenStable();
    expect(fixture.nativeElement.textContent).toContain('会議室を予約する(担当: 鈴木)');
  });

  it('404 のとき error 状態になり、value() は例外を投げる', async () => {
    const { fixture, app } = setup();
    http.expectOne('/api/tasks').flush([]);

    app.selectedId.set(99);
    TestBed.tick();
    http.expectOne('/api/tasks/99?delay=800')
      .flush({ message: 'task 99 not found' }, { status: 404, statusText: 'Not Found' });
    await fixture.whenStable();

    expect(app.task.status()).toBe('error');
    expect(app.task.statusCode()).toBe(404);
    expect(app.owner.status()).toBe('error'); // chain() でエラーが伝わる
    expect(() => app.task.value()).toThrow();
    expect(app.task.hasValue()).toBe(false);
  });
});
 ✓ |res-app| src/app/app.spec.ts > App(httpResource) > 一覧を取得して表示する 48ms
 ✓ |res-app| src/app/app.spec.ts > App(httpResource) > ID を切り替えると前のリクエストはキャンセルされる 11ms
 ✓ |res-app| src/app/app.spec.ts > App(httpResource) > 404 のとき error 状態になり、value() は例外を投げる 10ms
 Test Files  1 passed (1)
      Tests  3 passed (3)

テストでつまずいた点

症状 原因 対策
expectOne が「found none」で失敗 リソースのリクエストは変更検知(effect)の中で送られる TestBed.tick() を呼んでから expectOne
依存リクエスト(chain)が「found none」 flush() の結果がリソースに反映されるのは次のマイクロタスク await Promise.resolve() → TestBed.tick() の順にしたら通過(実測)
await fixture.whenStable() がタイムアウト 保留中の HTTP があるとアプリが安定しない 先に flush() してから whenStable
first.cancelled が false ID を変えただけでは再計算されていない set() のあとに TestBed.tick()

表の2行目の「await Promise.resolve()」は、今回の Angular 22.2.0 で通った方法です。環境によって必要な待ち方が変わる可能性があるため、落ちたときは TestBed.tick() と await fixture.whenStable() の位置を見直してください。

よくあるつまずき

症状・疑問 原因 対処
value() を読んだら例外 error 状態で value() を読んだ テンプレートは @if (res.hasValue()) で囲む
URL に undefined が入る 条件がそろう前に URL を組み立てた 関数から undefined を返して idle にする
前の値が一瞬消える params が変わると loading で値が defaultValue に戻る 仕様どおり。値を残すのは reload() のとき
POST をリソースで送りたい リソースは読み込み用 HttpClient.post() を直接使い、成功後に reload()
request / loader が型エラー v19 時代の名前 v22 は params、rxResource は stream
fetch が中断されない abortSignal を渡していない fetch(url, { signal: abortSignal })
インターセプターが効かない provideHttpClient を書いていない provideHttpClient(withInterceptors([...])) を追加

よくある質問

Q. httpResource はいつ安定版になりましたか? A. v22 です。@angular/common/http の型定義で httpResource・HttpResourceRef などが @publicApi 22.0、resource・rxResource も @publicApi 22.0 になっています。debounced と resourceFromSnapshots は v22 時点でも experimental です。

Q. httpResource はキャッシュしてくれますか? A. 公式ガイドには、画面をまたいで結果を共有するキャッシュ機能の説明はありません。用意されているのは、SSR でサーバーの取得結果をブラウザで再利用する仕組み(HttpClient の transfer cache、resource の id オプション)です。複数の画面で同じ結果を使い回したい場合は、リソースをサービス(providedIn: 'root')に置いて共有するのが基本です。

Q. httpResource と toSignal(http.get()) はどう違いますか? A. toSignal は一度だけ購読し、読み込み中やエラーの状態を持ちません。httpResource は signal の変化で取り直し、status・error・isLoading・reload を最初から備えています。ID などに応じて取り直す画面では httpResource が短く書けます。

Q. resource の loader で HttpClient を使ってもよいですか? A. 使えますが、Observable を Promise に変える手間があるので、HttpClient なら httpResource か rxResource のほうが自然です。resource は fetch や IndexedDB など Promise を返す API に向いています。

まとめ

  • httpResource・resource・rxResource は v22 で安定版。GET は httpResource、Observable は rxResource、Promise は resource
  • signal を読む関数を渡すだけで自動再取得、前のリクエストは中断される(実測で ABORTED を確認)
  • 状態は idle / loading / reloading / resolved / local / error の6つ。reload() は値を保持、params 変更は値がリセット
  • chain() で依存リクエストの loading・error が下流にも伝わる
  • parse で zod などの検証を挟める。ただしバンドルサイズの増加も確認する
  • value() は error 状態で例外になるので、hasValue() で囲む
  • テストは provideHttpClientTesting+TestBed.tick()+flush()、whenStable は flush の後に

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angular httpResource使い方|v22安定版の実例とテスト」

株式会社RJCでは、新しく安定版になった API を手元で動かして挙動を確かめ、わかったことをメンバー同士で共有しながら開発に生かしています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?