使用 TanStack Angular Query
为了支持Angular,TanStack Query正在开发适配器来提供API。
由于我在React开发中有过良好的开发经验,我对Angular也产生了兴趣并尝试了一下。
此外,本文不推荐在产品中使用它。
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,
},
},
})
),
],
};
查询和变更的实现
注入查询
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与injectQuery不同,可以在任何时间点执行mutationFn。通过将injectQuery和相同的key作为queryClient.setQueryData()的参数,可以更新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来获取通信中的状态。
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 可以以唯一的方式管理异步处理的状态,并且可以通过 queryClient.setQueryData() 更新异步处理结果,而不需要创建额外的 Signals 来拼接查询结果,这是非常有吸引力的。同时,使用 Signals 为基础的 API 还与现代化的 Angular 开发非常相容,这也是我感到欣喜的地方。
而且,我也对通过使用 HttpContext + Interceptor 来自行实现 HTTP 请求的缓存控制的前后处理方式并不感兴趣,我更喜欢从提供的 QueryClientConfig 中进行设置这种新颖的方法。
除了只使用Angular和TanStack Query,我们还会深入探讨在与直接使用HttpClient相比时的优势(如果有兴趣的话,还可能会写续篇)。非常感谢!