TanStack Query v5 完整教學:React 伺服器狀態管理與資料快取最佳實踐
為什麼你需要 TanStack Query
如果你寫過 React 的資料抓取邏輯,一定對這種程式碼不陌生:useState 管 loading、useState 管 error、useState 管 data、useEffect 裡面包 fetch、加上 cleanup function 防止 memory leak... 光是一個 API 呼叫就要寫二十幾行程式碼,而且這還沒處理快取、重試、背景刷新等需求。
TanStack Query(前身為 React Query)就是為了解決這個痛點而生的。它專注處理伺服器狀態(Server State)——也就是那些來自 API、資料庫等外部來源的資料。跟 Zustand 或 Redux 管理的客戶端狀態不同,伺服器狀態有幾個獨特的挑戰:
- 資料不在你的控制範圍內,隨時可能被其他人修改
- 需要非同步取得,存在延遲和錯誤的可能
- 可能在你不知道的情況下變成過期(stale)資料
- 需要在多個元件之間共享同一份資料
TanStack Query v5 把這些問題全部優雅地解決了。如果你正在用 Next.js App Router 全面實戰,搭配 TanStack Query 更是如虎添翼。
安裝與基本設定
先裝套件:
npm install @tanstack/react-query @tanstack/react-query-devtools
接著在你的 App 根元件設定 QueryClientProvider:
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export default function Providers({ children }: { children: React.ReactNode }) {
// 用 useState 避免 SSR 時共享 QueryClient
const [queryClient] = useState(() => new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // 資料 60 秒內視為新鮮
gcTime: 5 * 60 * 1000, // 快取保留 5 分鐘(v5 改名)
retry: 2, // 失敗重試 2 次
refetchOnWindowFocus: true, // 切回視窗時自動重抓
},
},
}));
return (
{children}
);
}
v5 有個重要的改名:cacheTime 改成了 gcTime(garbage collection time)。名字改得更精確了,但升級的時候容易忘記改這裡。
useQuery 核心用法與參數解析
useQuery 是你最常用到的 hook。v5 最大的變更是改成只接受物件參數:
import { useQuery } from '@tanstack/react-query';
interface User {
id: number;
name: string;
email: string;
}
function UserProfile({ userId }: { userId: number }) {
const { data, isLoading, isError, error, isFetching } = useQuery({
queryKey: ['user', userId],
queryFn: async (): Promise<User> => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
},
enabled: !!userId, // userId 存在才發請求
staleTime: 5 * 60 * 1000, // 這筆資料 5 分鐘不過期
select: (data) => ({ // 轉換回傳資料
...data,
displayName: data.name.toUpperCase(),
}),
});
if (isLoading) return <div>載入中...</div>;
if (isError) return <div>錯誤:{error.message}</div>;
return (
<div>
<h1>{data?.displayName}</h1>
<p>{data?.email}</p>
{isFetching && <span>背景更新中...</span>}
</div>
);
}
queryKey 是 TanStack Query 的靈魂。它不只是一個識別符,更是自動快取和自動重抓的依據。當 userId 改變時,TanStack Query 會自動抓取新的資料,而舊的快取依然保留。
useMutation 處理資料變更
讀取資料用 useQuery,寫入資料就靠 useMutation:
import { useMutation, useQueryClient } from '@tanstack/react-query';
function UpdateUserForm({ userId }: { userId: number }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newData: Partial<User>) => {
const res = await fetch(`/api/users/${userId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newData),
});
if (!res.ok) throw new Error('更新失敗');
return res.json();
},
onSuccess: (data) => {
// 更新成功後,讓相關的 query 重新抓取
queryClient.invalidateQueries({ queryKey: ['user', userId] });
// 或者直接更新快取(更快,不用多一次請求)
queryClient.setQueryData(['user', userId], data);
},
onError: (error) => {
console.error('更新失敗:', error);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate({ name: '新名字' });
};
return (
<form onSubmit={handleSubmit}>
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? '更新中...' : '更新'}
</button>
{mutation.isError && <p>錯誤:{mutation.error.message}</p>}
</form>
);
}
注意 v5 把 isLoading 在 mutation 裡改成了 isPending,語意更清楚了。
樂觀更新與快取失效策略
樂觀更新(Optimistic Update)是讓 UI 感覺超快的秘密武器。先更新畫面,再等伺服器確認,失敗的話再回滾:
const mutation = useMutation({
mutationFn: updateUser,
onMutate: async (newData) => {
// 取消正在進行的查詢,避免覆蓋樂觀更新
await queryClient.cancelQueries({ queryKey: ['user', userId] });
// 保存舊資料(用來回滾)
const previousUser = queryClient.getQueryData(['user', userId]);
// 樂觀更新快取
queryClient.setQueryData(['user', userId], (old: User) => ({
...old,
...newData,
}));
return { previousUser };
},
onError: (err, newData, context) => {
// 出錯了,回滾到舊資料
queryClient.setQueryData(['user', userId], context?.previousUser);
},
onSettled: () => {
// 不管成功失敗,都重新驗證一次
queryClient.invalidateQueries({ queryKey: ['user', userId] });
},
});
快取失效是另一個重要主題。TanStack Query 提供了精細的控制方式,你可以用 React Compiler 搭配確保效能不會因為過多的重新渲染而下降。
無限滾動與分頁查詢
TanStack Query 內建了 useInfiniteQuery,處理無限滾動超級方便:
import { useInfiniteQuery } from '@tanstack/react-query';
function PostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: async ({ pageParam }) => {
const res = await fetch(`/api/posts?cursor=${pageParam}`);
return res.json();
},
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
});
return (
<div>
{data?.pages.map((page) =>
page.items.map((post: any) => (
<article key={post.id}>{post.title}</article>
))
)}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? '載入中...' : hasNextPage ? '載入更多' : '沒有更多了'}
</button>
</div>
);
}
v5 新增了必填的 initialPageParam 參數,升級時別忘了加。
v5 重大變更與遷移指南
從 v4 升級到 v5,有幾個重大變更要注意:
| v4 | v5 | 說明 |
|---|---|---|
| useQuery(key, fn, opts) | useQuery({ queryKey, queryFn, ...opts }) | 只接受物件參數 |
| cacheTime | gcTime | 更精確的命名 |
| isLoading (mutation) | isPending | 語意更清楚 |
| onSuccess/onError (useQuery) | 已移除 | 改用 useEffect 或全域設定 |
| 不需要 initialPageParam | initialPageParam 必填 | 明確指定初始分頁參數 |
| getLogger | 已移除 | 使用標準 console |
最容易踩坑的是 onSuccess 從 useQuery 裡被移除了。官方的理由是它會造成不必要的 side effect 和競態條件。如果你的測試用到了這個功能,搭配 Vitest 單元測試 來驗證行為是否正確。
實務最佳實踐與常見陷阱
經過多個專案的實戰經驗,我總結了幾條最佳實踐:
1. Query Key 要結構化:用陣列而非字串,把相關的參數都放進去。這樣 invalidateQueries 時可以用前綴匹配一次失效多個查詢。
// 好的做法
queryKey: ['users', userId, 'posts', { status: 'published' }]
// 這樣就能一次失效某個 user 的所有相關查詢
queryClient.invalidateQueries({ queryKey: ['users', userId] })
2. 把 queryFn 抽出來:不要把 fetch 邏輯直接寫在元件裡,抽成獨立的函式或自訂 hook,方便測試和復用。
3. 善用 staleTime:預設的 staleTime 是 0,代表資料永遠是 stale 的。根據你的業務需求設定合理的值,可以大幅減少不必要的 API 請求。
4. 不要所有狀態都用 TanStack Query:它專為伺服器狀態設計。表單狀態、UI 狀態(modal 開關、主題切換)不應該放進來。
5. DevTools 是你的好朋友:ReactQueryDevtools 可以即時查看所有 query 的狀態、快取內容、refetch 時機。開發時務必開啟,debug 效率提升好幾倍。
TanStack Query v5 是目前 React 生態系裡最成熟的伺服器狀態管理方案。掌握它的核心概念,你的前端開發體驗會有質的飛躍。
繼續閱讀
Zustand vs Redux 2026 比較:React 狀態管理輕量方案到底該選誰?
深入比較 2026 年 Zustand 與 Redux 在 React 狀態管理上的差異,透過實際程式碼展示兩者的寫法差異,分析 Zustand Slice Pattern、Middleware 生態系與效能優勢,幫助開發者做出最佳選擇。
相關文章
你可能也喜歡
探索其他領域的精選好文