🎉

Angular Signals時代のHTTPリクエスト管理を考える

に公開

Angular Signals時代のHTTPリクエスト管理

Angular 16以降で導入されたSignalsは、Angularの状態管理に新しい選択肢をもたらしました。この記事では、Signalsを使用してHTTPリクエストを効率的に管理する方法について、実践的な例を交えながら解説していきます。

HTTPリクエストの基本

まず、Signalsを使用して基本的なHTTPリクエストの実装を見てみましょう。もちろん、副作用のある処理を行わないなら ngOnInit で実装するのではなく、HTMLテンプレートに直接バインディングして async Pipeで表示を管理するのもひとつ。その場合、リクエストは、テンプレート描画時に行われます。

@Component({
  template:
  `@if (!isLoading()) { @for (item of users(); track item.id) { ... } }`,
  ...
})
export class UserComponent implements OnInit {
  private readonly http = inject(HttpClient);
  readonly users = signal<User[]>([]);
  readonly isLoading = signal<boolean>(false);

  ngOnInit() {
    this.isLoading.set(true);
    this.http.get<User[]>('/api/users').subscribe({
      next: (data) => {
        this.isLoading.set(false);
        this.users.set(data);
    });
  }
}

なお、データ更新等のために再リクエストを行うことはできません。再リクエストが必要なら、自分で Observable を作って、 next する必要があったりとちょっと工夫が必要です。

@Component({...})
export class ExampleComponent {
  private refreshTrigger = new Subject<void>();

  data$: Observable<any> = this.refreshTrigger.pipe(
    startWith(undefined),
    switchMap(() => this.http.get('/api/users'))
  );
}

もしclass内で値を使う場合は data$subscribe する必要があり、unsubscribe するためにAngularのライフサイクルを使うことが一般的でした。まぁ、これまで慣れてきた書き方ですよね。

Resource APIを使用した実装

Angular 17で導入された新しいResource APIを使用すると、より宣言的な方法でHTTPリクエストを管理できます。この場合も発火するのは、テンプレートがレンダリングされたタイミングですが、 resource APIは reload メソッドを具備してるので、再リクエストを行うのは容易です。

@Component({
  template: 
  `@if (!users.isLoading()) { @for (item of users.value(); track item.id) { ... } }`,
  ...
})
export class UserComponent {
  private http = inject(HttpClient);
  users = resource({
    loader: () => firstValueFrom(this.http.get<User[]>('/api/users'))
  })

  // これだけで再リクエスト。テンプレートからも可能
  reload = () => this.users.reload();
}

また、 request プロパティを利用すると、request プロパティにわたすSignalsが変更される都度、リクエストを行うことができます。

@Component({...})
export class UserComponent {
  private http = inject(HttpClient);
  userId = signal<number>(1);
  users = resource({
    request: () => ({userId: userId()}),
    loader: ({request}) => firstValueFrom(this.http.get<User[]>('/api/users/' + request.userId))
  })
}

Observable を使って管理してた頃と比べてとても簡素に書けるようになりましたよね。また、ライフサイクルやユーザの操作によって発火するのではなく、Signalsの状態と反応をベースにすることからピュアな処理を書きやすく、テスタビリティがよくなるというメリットもあります。

httpResource はどうなの?

上記では、 HttpClient をPromiseに変換してresource APIと併用してますが、このふたつを組み合わせた httpResource というAPIがexperimenalで用意されています。

@Component({...})
export class UserComponent {
  userId = signal<number>(1);
  users = httpResource('/api/users/' + this.userId());
}

中身は HttpRequest なので従来通り Interceptor もきいてすばらしいのですが、実際プロジェクトに導入すると少し苦労することになります。
リクエストで得た値を加工する必要があり、また複数コンポーネントで利用するならServiceに移して共通化したいですよね。例えば以下のような感じです。

@Service({...})
export class UserService {
  resourceUsers(userId: WritableSignal) {
    return httpResource('/api/users/' + this.userId(), {
        map: (data) => {
        // ここでいろいろ面倒な処理を行わないといけない
        return data;
    }
    });
  }
}

ただ、この resourceUsers は、Promiseというわけではないので await this.userService.resourceUsers(this.userId()) のように解決することはできません。ですので、もしも他のコンポーネントでユーザのクリックイベントの操作等によってこの値を扱いたかったら、事前にプロパティでロードを完了しておく必要があります。もしくは、Promiseでくくって、setInterval で値が入るまで待機。 toObservable でObservableに変換して、そのあとにPromiseに変換。どちらもちょっと現実的ではないですよね。

https://github.com/angular/angular/issues/58917

まだExperimentalなので今後何らかの解決方法が入る可能性はありますが、現時点ではプロジェクト全体の構成を考えると httpResouce の導入は慎重な検討が必要です。

実践的なHTTPリクエスト

コンポーネントを表示するためにリクエストを行ってページを表示するのも十分実践的ですが、もう一歩踏み込んで実装していきましょう。

無限スクロールの実装

無限スクロールを実装する例を考えていきましょう。仕様を考えると、 request に反応させるSignalsは、無限スクロールのページですね。何ページ目を取得するか(実装によっては、最後のアイテムidであったりします)をプロパティに持つために、page というWritableSignalを用意しましょう。また0ページを表示する場合は、

  • 表示する時のリクエスト
  • アイテムをリフレッシュする時のリクエスト

ですので、常にそのアイテムだけを表示する必要があります。一方で、無限スクロールを行う時は、取得したアイテムを既存アイテムに「追加」する必要がありますね。

@Component({...})
export class InfiniteScrollComponent {
  private http = inject(HttpClient);
  
  page = signal(0);
  items = signal<Item[]>([]);

  constructor() {
    effect(async () => {
      const page = this.page(); // これに反応させる
      const data = await firstValueFrom(this.http.get('/api/users/' + page));
      this.items.update(item => {
        if (page === 0) {
            return [...data]
        } else {
            return [...item, ...data]
        }
      })
    })
  }
  
  // 次のページをリクエスト
  loadMore = () => this.page.update(page => page + 1);
  refresh = () => this.page.set(0);
}

今までだとユーザのイベントに対して処理を書いていましたが、Signalsベースの管理になると、値の状態変化に対しての処理を書けるのでとてもシンプルに書けるようになりましたね。

まとめ

Angular Signalsを使用することで、HTTPリクエストの状態管理がより直感的になり、コードの可読性も向上します。特に以下の点で利点があります:

  • リアクティブな状態管理が容易
  • コンポーネントの状態が明確
  • 非同期処理の状態管理がシンプル
  • パフォーマンスの最適化が容易

Signalsは、Angularの新しい機能として、今後ますます活用されることが期待されます。HTTPリクエストの管理においても、Signalsを活用することで、より保守性の高いコードを書くことができます。

それではまた。

Discussion