前面我們花了不少時間討論,為什麼「請求本身的狀態」和「元件的顯示狀態」應該分開處理。
延續這個方向,這篇想從一個更根本的問題開始:TanStack Query 想解決什麼?而這些問題,在 Angular 的 httpResource() 中,又有哪些已經被處理了?
前端有些狀態完全由自己控制,例如目前開啟哪個 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 的替代關係。
下面同樣根據 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 是 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。
資料取得並存入 Cache 後,TanStack Query 還會繼續管理它的新鮮度、重新同步與清除時機。

staleTime:多久內算 freshstaleTime: 30_000
代表資料取得成功後的 30 秒內維持 fresh,超過這段時間後,資料就會變成 stale。
但 stale 不代表資料被刪除,而是表示這份資料之後遇到適合的時機時,可以再向 Server 取得最新內容。
TanStack Query 預設的 staleTime 是 0,也就是資料取得成功後會立即被視為 stale,但不會因此立刻再次發出 Request。
當 Query 已經 stale,並遇到 Query 再次被使用、使用者切回瀏覽器頁籤或網路恢復等時機,就可能觸發重新同步。
這時前端仍然可以繼續使用現有 Cache,同時在背景取得新資料,因此重新同步時,畫面不一定需要重新進入 Loading。
gcTime:沒人使用後保留多久gcTime: 5 * 60_000
代表 Query 變成 inactive,也就是已經沒有任何地方使用後,Cache 還會保留 5 分鐘。
如果這段時間內再次使用相同 Query Key,仍然可以找到原本的資料;超過 gcTime 後,Cache 才會被清除。
兩者處理的是不同階段:
staleTime:資料多久內維持 freshgcTime:Query 沒有人使用後,Cache 保留多久理解 Query Cache 後,可以把非同步資料拆成三個角度:

假設 User 1 已經存在 Cache,但目前是 stale。再次使用這個 Query 時,三種狀態可以同時成立:
Request 正在進行,不代表畫面沒有資料可以顯示。即使背景正在重新取得資料,只要已經有可用的 Cache,畫面就可以繼續顯示既有內容,而不是重新切回 Skeleton。
使用 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,
}));
兩者看起來相似,但使用的資料來源不同:
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 和 isFetchingTanStack 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。
如果資料只在單一頁面使用,離開後重新取得也沒關係,那麼 httpResource() 通常已經能處理大部分需求。
Query Cache 比較適合這些情境:
判斷時可以先從兩件事來看:
如果這兩種情況經常發生,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。如果要導入正式環境,還是需要鎖定版本並審慎升級。
injectQuery() 基本用法。staleTime 預設值與重新同步時機。isPending 與 isFetching。placeholderData: keepPreviousData 的用法。