【TanStack Query + Zustand 教學】#02 TanStack Query 基礎:useQuery 與自動快取

測驗:TanStack Query 基礎:useQuery 與自動快取

共 5 題,點選答案後會立即顯示結果

1. 使用 TanStack Query 時,QueryClientProvider 應該放在應用程式的哪個位置?

  • A. 只需要放在使用 useQuery 的元件內部
  • B. 放在每個需要查詢的頁面元件中
  • C. 放在應用程式的最外層,包裹所有需要使用查詢的元件
  • D. 放在 index.html 的 script 標籤中

2. useQuery 的兩個必要參數是什麼?

  • A. dataerror
  • B. queryKeyqueryFn
  • C. staleTimegcTime
  • D. isLoadingisFetching

3. 關於 isLoadingisFetching 的差異,下列敘述何者正確?

  • A. 兩者完全相同,可以互換使用
  • B. isLoading 在背景更新時為 true,isFetching 只在第一次載入時為 true
  • C. isLoading 只在第一次載入且沒有快取資料時為 true,isFetching 只要正在發送請求就為 true
  • D. isLoading 表示錯誤狀態,isFetching 表示成功狀態

4. staleTime 的預設值是多少?這代表什麼意義?

  • A. 預設值是 0,表示資料一取得就立刻過期
  • B. 預設值是 5 分鐘,表示資料會保持新鮮 5 分鐘
  • C. 預設值是 Infinity,表示資料永遠不會過期
  • D. 沒有預設值,必須手動設定

5. 關於 Query Key 的設計,下列哪種做法是文章建議的?

// 選項 A queryKey: ‘users’ // 選項 B queryKey: [‘users’, userId] // 選項 C queryKey: { type: ‘users’, id: userId } // 選項 D queryKey: `users-${userId}`
  • A. 使用單一字串作為 queryKey
  • B. 使用陣列結構,把依賴參數放入 queryKey
  • C. 使用物件作為 queryKey 的根結構
  • D. 使用模板字串組合參數

前言

上一篇我們認識了 Server State 與 Client State 的差異,了解為什麼需要專門的工具來處理「來自伺服器的資料」。這篇文章將帶你實際動手,學會使用 TanStack Query 的核心功能:useQuery

當你在專案中看到這樣的程式碼時:

const { data, isLoading, error } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
});
Code language: JavaScript (javascript)

讀完這篇後,你將能夠完全理解這段程式碼的運作原理,以及背後的快取機制如何讓你的應用程式更有效率。


學習目標

讀完本篇後,你將能夠:

  • 安裝並設定 TanStack Query 環境
  • 使用 useQuery 取得遠端資料並處理 loading/error 狀態
  • 理解 Query Key 的設計原則與快取機制

安裝與基礎設定

安裝套件

npm install @tanstack/react-query
# 或
yarn add @tanstack/react-query
Code language: CSS (css)

設定 QueryClientProvider

TanStack Query 需要一個「查詢客戶端」來管理所有的快取和查詢狀態。這個設定通常放在應用程式的最外層:

// main.jsx 或 App.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

// 建立 QueryClient 實例
const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
    </QueryClientProvider>
  );
}
Code language: JavaScript (javascript)

讀程式碼時的觀察重點:

  • QueryClient 是快取的核心,整個應用程式共用一個實例
  • QueryClientProvider 透過 React Context 把 queryClient 傳遞給所有子元件
  • 任何使用 useQuery 的元件都必須在 QueryClientProvider 內部

useQuery 基本用法

最簡單的範例

import { useQuery } from '@tanstack/react-query';

function UserList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const response = await fetch('/api/users');
      return response.json();
    },
  });

  if (isLoading) return <div>載入中...</div>;
  if (error) return <div>發生錯誤:{error.message}</div>;

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
Code language: JavaScript (javascript)

useQuery 的兩個必要參數

參數 說明
queryKey 查詢的唯一識別碼,用於快取管理
queryFn 實際取得資料的非同步函式

讀程式碼時的理解方式:

  • queryKey: ['users']:這是這筆資料的「身分證」,TanStack Query 用它來決定要從快取拿資料還是重新請求
  • queryFn:當需要取得新資料時,會呼叫這個函式

回傳值解構

useQuery 回傳一個物件,常用的屬性包括:

const {
  data,       // 查詢成功後的資料
  isLoading,  // 是否正在載入(第一次載入)
  isFetching, // 是否正在取得資料(包含背景更新)
  error,      // 錯誤物件(如果有的話)
  isError,    // 是否發生錯誤
  isSuccess,  // 是否成功取得資料
  refetch,    // 手動重新取得資料的函式
} = useQuery({ ... });
Code language: JavaScript (javascript)

isLoading vs isFetching 的差異:

  • isLoading:只在「第一次載入」且「沒有快取資料」時為 true
  • isFetching:只要正在發送請求就為 true(包含背景更新)
// 實務上常見的判斷方式
if (isLoading) {
  // 顯示骨架畫面或 spinner(第一次載入)
}

// 如果你想在背景更新時也顯示提示
if (isFetching && !isLoading) {
  // 顯示「更新中」的小提示
}
Code language: JavaScript (javascript)

快取機制:staleTime 與 gcTime

TanStack Query 最強大的功能之一就是自動快取。理解這兩個時間設定,是掌握快取行為的關鍵。

staleTime(資料過期時間)

const { data } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  staleTime: 5 * 60 * 1000, // 5 分鐘
});
Code language: JavaScript (javascript)

staleTime 的意義:

  • 資料在這段時間內被視為「新鮮的」(fresh)
  • 新鮮的資料不會觸發背景更新
  • 預設值是 0,表示資料一取得就立刻過期

實務場景:

// 使用者資料很少變動,設定 5 分鐘
useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  staleTime: 5 * 60 * 1000,
});

// 即時股價需要頻繁更新,設定 10 秒
useQuery({
  queryKey: ['stock', symbol],
  queryFn: () => fetchStockPrice(symbol),
  staleTime: 10 * 1000,
});
Code language: JavaScript (javascript)

gcTime(垃圾回收時間)

const { data } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  gcTime: 10 * 60 * 1000, // 10 分鐘
});
Code language: JavaScript (javascript)

gcTime 的意義:

  • 當沒有任何元件使用這筆快取時,經過 gcTime 後會被清除
  • 預設值是 5 60 1000(5 分鐘)
  • 在 v4 之前稱為 cacheTime

快取生命週期圖解

使用者進入頁面
    ↓
[查詢開始] ──→ [資料取得] ──→ [資料新鮮期間]
                              (staleTime 內)
                                   ↓
                              [資料過期]
                              (staleTime 後)
                                   ↓
    ┌─────────────────────────────────────┐
    │ 觸發背景更新的條件:                   │
    │ • 元件重新掛載                        │
    │ • 視窗重新獲得焦點                    │
    │ • 網路恢復連線                        │
    │ • 手動呼叫 refetch                   │
    └─────────────────────────────────────┘
                                   ↓
                          [元件卸載/不再使用][gcTime 倒數開始][快取被清除]
Code language: CSS (css)

Query Key 設計模式

Query Key 是 TanStack Query 最重要的概念之一。它不只是識別碼,更決定了快取如何被管理和失效。

基本原則:使用陣列結構

// 好的設計:使用陣列,有層級關係
queryKey: ['users']
queryKey: ['users', userId]
queryKey: ['users', userId, 'posts']
queryKey: ['users', { status: 'active' }]

// 避免:單一字串(雖然技術上可行)
queryKey: 'users'  // 不建議
Code language: JavaScript (javascript)

依賴參數的處理

當查詢依賴某些參數時,把參數放入 queryKey:

function UserProfile({ userId }) {
  const { data } = useQuery({
    queryKey: ['users', userId],  // userId 是依賴參數
    queryFn: () => fetchUser(userId),
  });
}
Code language: JavaScript (javascript)

為什麼要這樣設計?

  • 不同的 userId 會有不同的快取
  • 當 userId 改變時,自動使用新的快取或發送新請求
  • 可以用 ['users'] 一次讓所有使用者相關的快取失效

包含篩選條件

function UserList({ filters }) {
  const { data } = useQuery({
    queryKey: ['users', filters],  // filters 物件也放入 key
    queryFn: () => fetchUsers(filters),
  });
}

// 使用範例
<UserList filters={{ status: 'active', role: 'admin' }} />
Code language: JavaScript (javascript)

讀程式碼時的觀察重點:

  • queryKey 包含了所有會影響查詢結果的參數
  • 物件會被深度比較,內容相同就會使用同一份快取

Query Key 的階層關係

// 這些 key 有階層關係
['todos']                    // 所有待辦事項
['todos', 'list']            // 待辦列表
['todos', 'list', { status: 'done' }]  // 已完成的待辦列表
['todos', 'detail', 1]       // 單一待辦詳情

// 可以一次讓整個階層失效
queryClient.invalidateQueries({ queryKey: ['todos'] });
// 上面這行會讓所有以 ['todos'] 開頭的查詢都失效
Code language: JavaScript (javascript)

DevTools 安裝與使用

TanStack Query 提供了強大的開發者工具,幫助你觀察快取狀態和除錯。

安裝

npm install @tanstack/react-query-devtools
Code language: CSS (css)

加入應用程式

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}
Code language: JavaScript (javascript)

DevTools 功能

開啟 DevTools 後,你可以:

  1. 查看所有快取的查詢:每個 queryKey 對應的資料和狀態
  2. 觀察狀態變化:fresh、stale、fetching、inactive 等狀態
  3. 手動觸發動作:refetch、invalidate、reset、remove
  4. 檢視查詢細節:最後更新時間、資料內容、錯誤訊息

除錯技巧:

  • 如果資料沒有更新,檢查 staleTime 設定
  • 如果快取消失太快,檢查 gcTime 設定
  • 如果看到重複請求,檢查 queryKey 是否正確

完整範例

讓我們把學到的概念整合成一個完整範例:

// api.js - 定義 API 函式
export const fetchUsers = async (filters) => {
  const params = new URLSearchParams(filters);
  const response = await fetch(`/api/users?${params}`);
  if (!response.ok) {
    throw new Error('取得使用者列表失敗');
  }
  return response.json();
};

export const fetchUser = async (userId) => {
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) {
    throw new Error('取得使用者資料失敗');
  }
  return response.json();
};
Code language: JavaScript (javascript)
// hooks/useUsers.js - 封裝 Query Hook
import { useQuery } from '@tanstack/react-query';
import { fetchUsers, fetchUser } from '../api';

export function useUsers(filters) {
  return useQuery({
    queryKey: ['users', filters],
    queryFn: () => fetchUsers(filters),
    staleTime: 60 * 1000, // 1 分鐘
  });
}

export function useUser(userId) {
  return useQuery({
    queryKey: ['users', userId],
    queryFn: () => fetchUser(userId),
    staleTime: 5 * 60 * 1000, // 5 分鐘
    enabled: !!userId, // 只有當 userId 存在時才執行查詢
  });
}
Code language: JavaScript (javascript)
// components/UserList.jsx
import { useUsers } from '../hooks/useUsers';

function UserList({ filters }) {
  const { data, isLoading, error, isFetching } = useUsers(filters);

  if (isLoading) {
    return <div className="skeleton">載入中...</div>;
  }

  if (error) {
    return <div className="error">錯誤:{error.message}</div>;
  }

  return (
    <div>
      {isFetching && <span className="updating">更新中...</span>}
      <ul>
        {data.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}
Code language: JavaScript (javascript)

讀程式碼時的重點整理:

  1. API 函式與 Query Hook 分離,職責清晰
  2. enabled 選項可以控制查詢是否執行
  3. isLoading 處理初次載入,isFetching 處理背景更新
  4. 錯誤處理透過 throw Error,TanStack Query 會自動捕捉

本篇小結

這篇文章涵蓋了 TanStack Query 的核心基礎:

概念 重點
QueryClientProvider 應用程式最外層設定,提供快取管理
useQuery 取得資料的主要 Hook,回傳 data、isLoading、error 等
queryKey 快取的唯一識別碼,使用陣列結構
queryFn 實際取得資料的非同步函式
staleTime 資料保持新鮮的時間,過期後觸發背景更新
gcTime 快取被清除前的保留時間
DevTools 開發階段觀察快取狀態的好幫手

下一篇預告

下一篇我們將學習 useMutation,處理新增、更新、刪除等「會改變伺服器資料」的操作,以及如何在操作後讓相關快取失效,保持資料一致性。

進階測驗:TanStack Query 基礎:useQuery 與自動快取

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你正在開發一個股票報價頁面,需要顯示即時股價。考慮到股價變動頻繁,你應該如何設定 staleTime情境題

useQuery({ queryKey: [‘stock’, symbol], queryFn: () => fetchStockPrice(symbol), staleTime: ???, });
  • A. 設定為 0(預設值),讓資料永遠過期
  • B. 設定為 10 秒或更短,確保頻繁更新
  • C. 設定為 5 分鐘,減少 API 請求次數
  • D. 設定為 Infinity,避免不必要的重新請求

2. 你需要在使用者列表頁面中實作篩選功能。當篩選條件改變時,你希望自動使用新的快取或發送新請求。應該如何設計 Query Key? 情境題

function UserList({ filters }) { const { data } = useQuery({ queryKey: ???, queryFn: () => fetchUsers(filters), }); }
  • A. queryKey: ['users']
  • B. queryKey: ['users', 'list']
  • C. queryKey: ['users', filters]
  • D. queryKey: [filters]

3. 你在開發使用者詳情頁面,但發現當 userId 還沒有值時(例如來自 URL 參數但還沒解析完),會發送一個無效的 API 請求。如何避免這個問題? 情境題

function UserProfile({ userId }) { // userId 可能一開始是 undefined const { data } = useQuery({ queryKey: [‘users’, userId], queryFn: () => fetchUser(userId), // 需要加入什麼選項? }); }
  • A. 加入 staleTime: Infinity 避免重複請求
  • B. 加入 enabled: !!userId 只在 userId 存在時執行查詢
  • C. 加入 retry: false 避免錯誤重試
  • D. 加入 gcTime: 0 讓無效快取立即清除

4. 小明在使用 TanStack Query 時遇到錯誤。他的程式碼如下,但 useQuery 無法正常運作:錯誤診斷

// App.jsx import { useQuery } from ‘@tanstack/react-query’; function App() { const { data, isLoading } = useQuery({ queryKey: [‘users’], queryFn: fetchUsers, }); if (isLoading) return <div>載入中…</div>; return <div>{data.length} 位使用者</div>; } export default App;
  • A. queryKey 應該使用字串而非陣列
  • B. fetchUsers 應該定義為箭頭函式
  • C. 缺少 staleTime 設定
  • D. 缺少 QueryClientProvider 包裹應用程式

5. 小華發現他的應用程式在切換頁面後,快取資料很快就消失了,每次返回同一頁都要重新載入。他的設定如下,問題最可能出在哪裡?錯誤診斷

const { data } = useQuery({ queryKey: [‘products’], queryFn: fetchProducts, staleTime: 5 * 60 * 1000, // 5 分鐘 gcTime: 10 * 1000, // 10 秒 });
  • A. staleTime 設定太長,應該縮短
  • B. gcTime 設定太短,快取在元件卸載後很快就被清除
  • C. queryKey 設計不當,應該加入更多參數
  • D. 缺少 enabled 選項,導致查詢未被執行

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *