lacolaco's marginalia

Angular: httpResource Quick Overview

現在、Angularの新しい実験的API httpResource が開発されている。これはv19.0でリリースされた実験的API resource と関連する機能で、早ければ2月のv19.2、あるいは3月のv19.3で搭載されるだろう。まだリリース前だが、現時点での要点をかいつまんで紹介する。

Usage

httpResourceはひとことで言えば、「HttpClient + resource の一般的なユースケースを簡略化するヘルパー関数」である。

使い方は次のようになるだろう。すでにresourceを使っている人からすればそれほど目新しくはない。httpResource関数の戻り値はHttpResponseResource型であり、これはResource型のサブタイプである。なのでresource関数の戻り値と同じように、isLoadingvalueといったシグナルを返すフィールドを持っている。シグナルなので、状態が変われば自動的にコンポーネントは再描画される。

@Component({ 
  template: `
  @if (data.isLoading()) {
    <p>Loading</p>
  } 
  @else {
    {{ data.value() }}
  }
  `
})
export class App {
  readonly data = httpResource<Data>('/api/data');
}

Request on Signal changes

第一引数にはHTTPリクエストを生成するための情報を渡す。文字列を渡せばURLとして扱われ、GETメソッドのリクエストが一度だけ送られる。関数を渡せば、その戻り値の文字列をURLとしてGETメソッドのリクエストが送られる。この関数はシグナルに対応しており、内包するシグナルの変更に反応してリクエストを再送信する。

たとえば、コンポーネントが親コンポーネントから受け取ったインプット値に対応したHTTPリクエストを送るなら次のようになる。

@Component({ 
  template: `
  @if (userData.isLoading()) {
    <p>Loading</p>
  } 
  @else {
    {{ userData.value() }}
  }
  `
})
export class App {
  readonly userId = input.required<number>();
  readonly userData = httpResource<UserData>(
    // this.userId が変わるたびにリクエストが送られて値が更新される
    () => `/api/user/${this.userId()}`,
  );
}

resource関数のrequestと同じように、この第一引数の関数がundefinedを返せばリクエストを送らずにキャンセルできる。初期状態ではリクエストせず追加のイベントを待つ場合に使われるだろう。

@Component({ 
  template: `
  @if (userData.isLoading()) {
    <p>Loading</p>
  } 
  @else {
    {{ userData.value() }}
  }
  `
})
export class App {
  readonly userId = signal<number>(-1);

  readonly userData = httpResource<UserData>(
    // undefinedを返すとリクエストが送信されない
    () => this.userId() < 0 ? undefiend : `/api/user/${this.userId()}`,
  );
}

HttpResourceRequest

あまり使わないと思われるが、GET以外のメソッドでHTTPリクエストを送ることもできる。文字列ではなくHttpResourceRequest型のオブジェクトを第一引数に渡すことでリクエストの内容を細かく制御できる。このオプションはHttpClientrequestメソッドの引数とほとんど同じである。オブジェクトを渡す場合も静的な値と関数の両方をサポートしている。

@Component(...)
export class App {
  // POST /data?fast=yes + headers + body + credentials
  readonly data = httpResource(
    () => ({
      url: '/data',
      method: 'POST',
      body: {message: 'Hello, backend!'},
      headers: {
        'X-Special': 'true',
      },
      params: {
        'fast': 'yes',
      },
      withCredentials: true,
    }),
  );
}

Response Value Mapping

第二引数のmapオプションでは、HTTPレスポンスボディに簡単な加工を加えてからvalueシグナルに格納するよう変換関数を渡すことができる。たとえばJSONオブジェクトからなんらかのクラスインスタンスへの変換をしたり、zodのようなバリデーション関数を挟んだりできる。

@Component(...)
export class App {
  readonly data = httpResource(`/api/user/${this.userId()}`, {
    map: (data) => User.parse(data),
  });
}

How it works

ソースコードを読めばわかるが、httpResourceは既存のHttpClientresourceを組み合わせただけのヘルパーだ。そのためHttpClientのインターセプターも変わらず動作するし、逆に言えば provideHttpClientHttpClient自体を利用可能にしていないと使えない。

また、resourceと同様に内部的にはeffectに依存している。つまり、依存性の注入が行えるコンテキストでなければ呼び出せない。コンポーネントのフィールド初期化、コンストラクタであれば普通に使えるが、それ以外の場所では工夫が必要になる。ちなみに、第2引数のinjectorオプションにInjectorオブジェクトを渡せばそのコンテキストで動作するようになっている。

// 任意の注入コンテキストでhttpResourceを呼び出す
const res = httpResource('/data', { injector: TestBed.inject(Injector) });

Use-cases

ここまで見たように、httpResourceは結局HttpClientでデータを解決するresourceを作成するため、まさにそのようなコードを書いていた部分ではボイラープレートを削減する助けになるだろう。HttpClientのメソッドはObservableを返すので、これまでは純粋なresourceではなくrxResourceを使うか、いちいちPromiseに変換する必要があったが、httpResourceであればそのあたりを気にする必要はなくなる。

一方、これまでresourceと関係なくHttpClientを使っていた処理をhttpResourceに書き換える必要があるかといえば、今のところは無いといっていいだろう。多くの場合はサービスクラスのメソッドでリクエストを送っていると思うが、そのような手続き的なコードからシグナルベースのリアクティブなコードに書き換えるのはなかなか骨が折れる大工事になる。

アプリケーション全体をリアクティブに書き換えていくことがあれば、resourcehttpResourceを取り入れていくくらいの構えでいいだろう。resourceの活用には前提としてアプリケーションのシグナルベース化、リアクティブ化が必要である。

Conclusion

以上見てきたように、httpResourceHttpClientresourceの組み合わせを簡略化する実験的APIだ。アプリケーションのリアクティブ化を進める中で、HTTPリクエストをシグナルベースで扱いたい場合に有用なツールとなるだろう。現時点では実験的な機能であるため、今後のAPIの変更には注意が必要だ。