使用 TanStack Angular Query

为了支持Angular,TanStack Query正在开发适配器来提供API。

 

由于我在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,
          },
        },
      })
    ),
  ],
};

查询和变更的实现

注入查询

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相比时的优势(如果有兴趣的话,还可能会写续篇)。非常感谢!

bannerAds