iT邦幫忙

2026 iThome 鐵人賽

DAY 12
1
Modern Web

Angular 22 Signal 進化論系列 第 12 篇

Day 12:從 httpResource 到 TanStack Query,認識 Server State

  • 分享至 

  • xImage
  •  

前面我們花了不少時間討論,為什麼「請求本身的狀態」和「元件的顯示狀態」應該分開處理。

延續這個方向,這篇想從一個更根本的問題開始:TanStack Query 想解決什麼?而這些問題,在 Angular 的 httpResource() 中,又有哪些已經被處理了?

TanStack Query 想解決什麼問題

前端有些狀態完全由自己控制,例如目前開啟哪個 Tab、Modal 是否顯示;但使用者列表、訂單、通知或文章清單不同,真正的資料存在 Server,前端拿到的只是某個時間點的副本,這類資料通常稱為 Server State。

當 Server 才是 Source of Truth,問題就不只是在「怎麼把資料抓回來」,還包括前端要怎麼辨識這份資料、是否要共用、多久算過期、什麼時候重新同步,以及最後何時清除。

TanStack Query 想解決什麼問題

TanStack Query 處理的不只是單次 Request,而是 Server State 從取得、快取、重新同步到清除的整個過程。queryKey、staleTime、gcTime 和 refetch 等概念,也都是圍繞這些問題設計。

httpResource() 和 TanStack Query 的管理範圍

httpResource() 已經把資料來源與非同步狀態整合進 Resource。

例如:

userId = signal(1);

userResource = httpResource<User>(
  () => `https://jsonplaceholder.typicode.com/users/${this.userId()}`
);

當 userId() 改變時,它會重新取得對應資料,並提供 value()、isLoading()、error()、status() 與 reload()。

httpResource 與 TanStack Query 的管理範圍

httpResource() 比較聚焦在單一 Resource:資料怎麼取得,以及目前處於什麼狀態。

TanStack Query 管理的範圍更大,除了資料取得之外,還會處理不同 Query 的辨識、快取、新鮮度與重新同步。兩者處理的是不同層級的問題,並不是新舊 API 的替代關係。

先看一個 TanStack Query 的例子

下面同樣根據 userId 取得使用者:

import { HttpClient } from '@angular/common/http';
import { Component, inject, signal } from '@angular/core';
import { injectQuery } from '@tanstack/angular-query-experimental';
import { firstValueFrom } from 'rxjs';

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

@Component({
  selector: 'app-root',
  template: `
    <button (click)="userId.set(1)">User 1</button>
    <button (click)="userId.set(2)">User 2</button>

    @if (userQuery.isPending()) {
      <p>Loading...</p>
    }

    @if (userQuery.isError()) {
      <p>取得失敗</p>
    }

    @if (userQuery.data(); as user) {
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
    }
  `,
})
export class AppComponent {
  private readonly http = inject(HttpClient);

  readonly userId = signal(1);

  readonly userQuery = injectQuery(() => ({
    queryKey: ['user', this.userId()],
    queryFn: () =>
      firstValueFrom(
        this.http.get<User>(
          `https://jsonplaceholder.typicode.com/users/${this.userId()}`
        )
      ),
    staleTime: 30_000,
    gcTime: 5 * 60_000,
  }));
}

這段程式和 httpResource() 一樣,會隨著 userId() 改變取得新資料。

差別在於,TanStack Query 還會描述這份資料的身分,以及它在 Cache 中接下來要怎麼被管理。

Query Key 與 Query Cache

Query Key 是 TanStack Query 用來辨識一份 Server State 的方式。

例如:

  • ['user', 1] 對應 User 1
  • ['user', 2] 對應 User 2

當 userId 從 1 切換成 2 時,TanStack Query 會把它們視為兩份不同的 Query,並分別保存各自的資料。

如果之後再次切回 User 1,就能透過:

['user', 1]

找到先前留下的 Cache。

一個 Query 最基本包含兩個部分:

  • queryKey:這是哪一份 Server State?
  • queryFn:需要時要怎麼取得它?

Query Key 讓 TanStack Query 能辨識不同資料,也讓多個使用位置可以共用同一份 Query。

一份 Query 的生命週期

資料取得並存入 Cache 後,TanStack Query 還會繼續管理它的新鮮度、重新同步與清除時機。

一份 Query 的生命週期

staleTime:多久內算 fresh

staleTime: 30_000

代表資料取得成功後的 30 秒內維持 fresh,超過這段時間後,資料就會變成 stale。

但 stale 不代表資料被刪除,而是表示這份資料之後遇到適合的時機時,可以再向 Server 取得最新內容。

TanStack Query 預設的 staleTime 是 0,也就是資料取得成功後會立即被視為 stale,但不會因此立刻再次發出 Request。

Refetch:在適合的時機重新同步

當 Query 已經 stale,並遇到 Query 再次被使用、使用者切回瀏覽器頁籤或網路恢復等時機,就可能觸發重新同步。

這時前端仍然可以繼續使用現有 Cache,同時在背景取得新資料,因此重新同步時,畫面不一定需要重新進入 Loading。

gcTime:沒人使用後保留多久

gcTime: 5 * 60_000

代表 Query 變成 inactive,也就是已經沒有任何地方使用後,Cache 還會保留 5 分鐘。

如果這段時間內再次使用相同 Query Key,仍然可以找到原本的資料;超過 gcTime 後,Cache 才會被清除。

兩者處理的是不同階段:

  • staleTime:資料多久內維持 fresh
  • gcTime:Query 沒有人使用後,Cache 保留多久

Request State、Server State 和 UI State

理解 Query Cache 後,可以把非同步資料拆成三個角度:

  • Request State:現在有沒有正在取得資料?
  • Server State:Cache 是否存在、資料是 fresh 還是 stale、是否需要重新同步?
  • UI State:畫面要顯示 Skeleton、既有資料,還是更新提示?

同一時間存在的三種狀態

假設 User 1 已經存在 Cache,但目前是 stale。再次使用這個 Query 時,三種狀態可以同時成立:

  • Server State:有 User 1 的 Cache,但資料是 stale
  • Request State:正在背景重新取得 User 1
  • UI State:繼續顯示目前的 User 1

Request 正在進行,不代表畫面沒有資料可以顯示。即使背景正在重新取得資料,只要已經有可用的 Cache,畫面就可以繼續顯示既有內容,而不是重新切回 Skeleton。

保留上一筆資料與 Query Cache

使用 httpResource() 時,也可以在參數改變、重新載入期間暫時保留上一筆 value:

userResource = withPreviousValue(
  httpResource<User>(
    () =>
      `https://jsonplaceholder.typicode.com/users/${this.userId()}`
  )
);

使用 httpResource() 時,也可以在參數改變、重新載入期間暫時保留上一筆 value。這種做法處理的是新資料還沒回來時,畫面要不要先顯示上一筆結果。

TanStack Query 也可以透過 placeholderData: keepPreviousData 處理相似的情境:

readonly userQuery = injectQuery(() => ({
  queryKey: ['user', this.userId()],
  queryFn: () =>
    firstValueFrom(
      this.http.get<User>(
        `https://jsonplaceholder.typicode.com/users/${this.userId()}`
      )
    ),
  placeholderData: keepPreviousData,
}));

兩者看起來相似,但使用的資料來源不同:

  • Query Cache:使用這個 Query Key 自己之前留下的資料
  • keepPreviousData:新的 Query 還沒有資料時,暫時使用上一個 Query 的結果

keepPreviousData 與 Query Cache

例如 User 1 和 User 2 都曾經取得過:

['user', 1] → User 1
['user', 2] → User 2

從 User 2 切回 User 1 時,TanStack Query 會使用 ['user', 1] 自己的 Cache,而不是沿用 User 2。

如果新的 Query 從來沒有取得過資料,keepPreviousData 才會使用上一個 Query 的結果作為 placeholder。

isPending 和 isFetching

TanStack Query 會把 Query 目前的資料狀態,和 queryFn 是否正在執行分開表示。

isPending 與 isFetching

isPending()

第一次進入 User 1,而且 Query 還沒有成功取得資料時:

data        → 沒有
isPending   → true
isFetching  → true

這種情況通常會顯示 Skeleton。

isFetching()

isFetching() 表示 queryFn 目前正在執行。第一次載入時會是 true,背景重新同步時也可能是 true。

如果 Query 已經有資料,只是在背景更新:

data        → 有 User 1
isPending   → false
isFetching  → true

這時可以繼續顯示原本內容,再補上一個較輕量的同步提示:

@if (userQuery.isFetching() && !userQuery.isPending()) {
  <small>更新中...</small>
}

這樣背景重新同步時,就不需要再把畫面切回 Skeleton。

什麼時候需要 Query Cache

如果資料只在單一頁面使用,離開後重新取得也沒關係,那麼 httpResource() 通常已經能處理大部分需求。

Query Cache 比較適合這些情境:

  • 同一份 Server State 會在多個元件中使用
  • 使用者回到畫面時,希望先看到之前取得的資料
  • 需要管理 fresh、stale 與背景重新同步
  • 新增、修改或刪除後,需要讓相關 Query 一起失效或更新

判斷時可以先從兩件事來看:

  1. 這份資料離開畫面後,還需不需要保留?
  2. 同一份資料會不會被其他地方共用,或在更新後需要一起重新同步?

如果這兩種情況經常發生,Query Cache 就會比單一 Resource 更適合。

本日結語

httpResource() 和 TanStack Query 處理的是不同層級的問題。httpResource() 聚焦在 HTTP 資料怎麼取得,以及 Resource 目前處於什麼狀態;TanStack Query 則把範圍延伸到 Server State 取得之後的管理。

當資料需要被不同元件共用,或需要處理 Cache、fresh、stale、背景重新同步與失效更新時,Query Cache 的價值就會比較明顯。這時管理的已經不只是一次 Request,而是這份 Server State 在前端的整個生命週期。

目前 TanStack Query 的 Angular 版本仍是 experimental,官方也提醒 minor 與 patch 版本都可能出現 breaking changes。如果要導入正式環境,還是需要鎖定版本並審慎升級。

資料來源


上一篇
Day 11:用 rxResource 串起 Observable 與 Resource
系列文
Angular 22 Signal 進化論 共 12 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言