Day 12:從 httpResource 到 TanStack Query,認識 Server State
5.1k 字,約 5 分鐘
2026 iThome 鐵人賽:Angular 22 Signal 進化論。 原文發表於 iThome,也可以在下方留言。
文章目錄
前面我們花了不少時間討論,為什麼「請求本身的狀態」和「元件的顯示狀態」應該分開處理。
延續這個方向,這篇想從一個更根本的問題開始:TanStack Query 想解決什麼?而這些問題,在 Angular 的 httpResource() 中,又有哪些已經被處理了?
TanStack Query 想解決什麼問題
前端有些狀態完全由自己控制,例如目前開啟哪個 Tab、Modal 是否顯示;但使用者列表、訂單、通知或文章清單不同,真正的資料存在 Server,前端拿到的只是某個時間點的副本,這類資料通常稱為 Server State。
當 Server 才是 Source of Truth,問題就不只是在「怎麼把資料抓回來」,還包括前端要怎麼辨識這份資料、是否要共用、多久算過期、什麼時候重新同步,以及最後何時清除。
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() 比較聚焦在單一 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 還會繼續管理它的新鮮度、重新同步與清除時機。
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:資料多久內維持 freshgcTime: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 的結果
例如 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()
第一次進入 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 一起失效或更新
判斷時可以先從兩件事來看:
- 這份資料離開畫面後,還需不需要保留?
- 同一份資料會不會被其他地方共用,或在更新後需要一起重新同步?
如果這兩種情況經常發生,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。如果要導入正式環境,還是需要鎖定版本並審慎升級。
資料來源
- TanStack Query 官方文件:Angular Overview:Angular 版本的實驗階段與
injectQuery()基本用法。 - TanStack Query 官方文件:Important Defaults:
staleTime預設值與重新同步時機。 - TanStack Query 官方文件:Caching Examples:Query Cache、inactive 與垃圾回收流程。
- TanStack Query 官方文件:Queries:Query Key、
isPending與isFetching。 - TanStack Query 官方文件:Paginated Queries:
placeholderData: keepPreviousData的用法。
留言
留言使用 GitHub 帳號登入(Giscus),留言會存在 GitHub Discussions。