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
でデータフェッチやキャッシュコントロールの設定をします。デフォルト値なので、リクエスト単位で変更することが可能です。
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を使っていません。
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
injectMutation
は injectQuery
とは異なり、任意のタイミングで mutationFn
を実行します。queryClient.setQueryData()
の引数に injectQuery
と同じkeyを渡すことで、 query.data()
の内容を更新することができます。
以下のサンプルコードではユーザ情報の更新を行います。
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
を使うことで通信中の状態を取得することが可能です。
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
を使う場合と比較した際の優位性についても深掘りしてみます(気が向いたら続編を書きます)。
ありがとうございました!