LoginSignup
3
0

TanStack Angular Queryを使う

Last updated at Posted at 2023-12-10

TanStack QueryではAngularのサポートに向け、APIを提供するためのadapterの開発が進められています。

Reactの開発で使用した時の開発体験が良かったこともあり、Angularではどうなんだろうと気になって触ってみました。

Angular Query(@tanstack/angular-query-experimental)は名前の通り実験的機能です。
また、本記事はプロダクトでの利用を推奨するものではありません。
https://tanstack.com/query/latest/docs/angular/overview

Angularのプロジェクトに導入する

パッケージをインストールします。devtoolsはお好みで(本記事では割愛します)。

% npm i @tanstack/angular-query-experimental

アプリケーションで QueryClient を使うため、 依存関係を設定します。 defaultOptions でデータフェッチやキャッシュコントロールの設定をします。デフォルト値なので、リクエスト単位で変更することが可能です。

app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';

import {
  QueryClient,
  provideAngularQuery,
} from '@tanstack/angular-query-experimental';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    // 動きを見るだけなので、デフォルトでQueryClientConfigの使わないオプションを切っています。
    // https://tanstack.com/query/latest/docs/react/reference/useQuery
    provideAngularQuery(
      new QueryClient({
        defaultOptions: {
          queries: {
            retry: false,
            gcTime: 0,
            retryOnMount: false,
          },
          mutations: {
            retry: false,
            gcTime: 0,
          },
        },
      })
    ),
  ],
};

Query, Mutationの実装

injectQuery

injectQuery は、 ngOnInit のようなコンポーネントのライフサイクル内でメソッドを叩くのではなく、インスタンス生成のタイミングで queryFn のPromiseが実行されます。
以下のサンプルコードでは queryFn でユーザ一覧取得のリクエストを行います。
※サンプルコードでは意図的にServiceを使っていません。

users.component.ts
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { Component, inject } from '@angular/core';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { firstValueFrom } from 'rxjs';

interface User {
  id: string;
  name: string;
}

@Component({
  selector: 'app-users',
  standalone: true,
  imports: [HttpClientModule],
  styleUrl: './users.component.scss',
  template: `
    @if (query.data(); as users) {
      @for(user of users; track user.id) {
        <h1>{{ user.name }}</h1>
      }
    }
  `,
})
export class UsersComponent {
  private readonly http = inject(HttpClient);

  // ユーザ一覧取得のQuery
  readonly query = injectQuery(() => ({
    queryKey: ['users'],
    queryFn: () => firstValueFrom(this.http.get<User[]>('/users')),
  }));

  constructor(){}
}

injectMutation

injectMutationinjectQuery とは異なり、任意のタイミングで mutationFn を実行します。queryClient.setQueryData() の引数に injectQuery と同じkeyを渡すことで、 query.data() の内容を更新することができます。
以下のサンプルコードではユーザ情報の更新を行います。

user-detail.component.ts
import { HttpClient, HttpClientModule } from '@angular/common/http';
import { Component, Signal, computed, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import {
  QueryKey,
  injectMutation,
  injectQuery,
} from '@tanstack/angular-query-experimental';
import { firstValueFrom, map } from 'rxjs';
import { UserInfo } from '../user-info/user-info.component';

interface UserInfo {
  name: string;
  age: number;
  memo: string;
}

interface UpdateUserInfoResponse {
  name: string;
  age: number;
  memo: string;
  updatedAt: string;
}

@Component({
  selector: 'app-edit-user',
  standalone: true,
  imports: [HttpClientModule, UserInfoComponent],
  styleUrl: './users.component.scss',
  template: `
    @if (query.data(); as userInfo) {
      <!-- ユーザ情報の表示と更新の操作を行うコンポーネント -->
      <app-user-info [userInfo]="userInfo" (onUpdateButtonClick)="updateUserInfo($event)"></app-user-info>
    }
  `,
})
export class EditUserComponent {
  private readonly http = inject(HttpClient);
  // pathparamsからユーザIDを取得する
  private readonly id = toSignal(
    inject(ActivatedRoute).paramMap.pipe(map((paramMap) => paramMap.get('id')))
  );
  // ユーザIDのSignalsを依存に持つQueryKeyのSignalsを定義する
  private readonly queryKey: Signal<QueryKey> = computed(() => [
    'users',
    this.id(),
  ]);

  // ユーザ情報取得のQuery
  readonly query = injectQuery(() => ({
    queryKey: this.queryKey(),
    queryFn: () => firstValueFrom(this.http.get<UserInfo>(`/users/${this.id()}`)),
  }));

  // ユーザ情報更新のMutation
  readonly mutation = injectMutation((queryClient) => ({
    mutationFn: (data: UserInfo) => firstValueFrom(this.http.put<UpdateUserInfoResponse>(`/users/${this.id()}`, data)),
    // リクエスト成功時にUpdateUserInfoResponseの内容でquery.data()を更新
    onSuccess: ({ name, age, memo }) => {
      queryClient.setQueryData(this.queryKey(), { name, age, memo });
    },
  }));

  constructor(){}

  updateUserInfo(data: UserInfo) {
    // mutationFnのPromiseを実行する
    this.mutation().mutate(data);
  }
}

injectIsFetching

通信中はheaderにprogress barを表示する のように、アプリケーション内で通信の監視が必要な場合、以下のように injectIsFetching を使うことで通信中の状態を取得することが可能です。

progress-bar.component.ts
import { Component, Signal, computed } from '@angular/core';
import { injectIsFetching } from '@tanstack/angular-query-experimental';

@Component({
  selector: 'app-progress-bar',
  standalone: true,
  imports: [],
  styleUrl: './progress-bar.component.scss',
  template: `
    @if (isFetching) { 
      <div class="progress-bar"></div>
    }
  `,
})
export class ProgressBarComponent {
  // injectIsFetchingの件数が0より大きいかどうかで判別する
  readonly isFetching: Signal<boolean> = computed(() => injectIsFetching()() > 0);

  constructor(){}
}

まとめ

非同期処理の状態を QueryClient + QueryKey でユニークに管理でき、非同期処理(query)の結果を詰めるためのSignalsを別で作らずに queryClient.setQueryData() で更新できるのは魅力的です。APIをSignalsベースで使える点も、モダンなAngular開発との親和性が高いと感じます。

また、HTTPリクエストのキャッシュコントロールを HttpContext + Interceptor でリクエスト前後の処理を自分で実装するのではなく、提供されている QueryClientConfig から設定するアプローチにも目新しさを感じます。

ただAngularでTanStack Queryを使うだけではなく、素で HttpClient を使う場合と比較した際の優位性についても深掘りしてみます(気が向いたら続編を書きます)。
ありがとうございました!

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