【React 純前端實戰】#03 TanStack Query:資料抓取與快取管理

測驗:TanStack Query 資料抓取與快取管理

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

1. 為什麼從 API 抓回來的資料(Server State)不適合只用 useState 管理?

  • A. 因為 useState 不支援儲存物件或陣列
  • B. 因為 useState 的效能比 TanStack Query 差
  • C. 因為 API 資料可能被別人修改、隨時過期,需要快取和自動重新抓取機制
  • D. 因為 useState 只能在 class component 中使用

2. 在 useQuery 中,queryKey 的作用是什麼?

useQuery({ queryKey: [‘posts’, 5], queryFn: () => fetchPost(5), });
  • A. 定義 API 的 URL 路徑
  • B. 作為這份資料的唯一識別,用來管理快取
  • C. 設定資料的型別檢查規則
  • D. 指定資料要存在哪個 React context 中

3. 使用者第一次進入頁面時 isLoading = true,離開後再回來時 isLoading = falseisFetching = true。這代表什麼?

  • A. 資料抓取失敗了,正在重試
  • B. 快取已經被清掉,必須從頭抓取
  • C. 使用者的網路連線不穩定
  • D. 有舊的快取資料可以先顯示,同時背景正在重新抓取最新資料

4. 在 useMutationonSuccess 中呼叫 invalidateQueries({ queryKey: ['posts'] }),會發生什麼事?

  • A. 立刻刪除所有 ['posts'] 相關的快取資料
  • B. 只會刷新 ['posts'] 這一個 key,不影響 ['posts', 5]
  • C. 所有以 ['posts'] 開頭的快取都會被標記為過期,並觸發重新抓取
  • D. 會清空整個 QueryClient 的所有快取

5. Optimistic Updates(樂觀更新)的核心三步驟,正確順序是什麼?

  • A. 送出 API 請求 → 等待回應 → 更新畫面
  • B. 在 onMutate 備份舊資料並先改快取 → 送 API → 失敗時在 onError 用備份回滾
  • C. 先改快取 → 送 API → 成功時才備份舊資料
  • D. 在 onSuccess 備份資料 → 改快取 → 在 onSettled 回滾

**系列**:React 純前端實戰(第 3 篇,共 5 篇)
**難度**:L1-起步
**前置知識**:讀完本系列第 1-2 篇、基本的 async/await 和 fetch API 概念

一句話說明

TanStack Query 幫你管理「從 API 抓回來的資料」——自動快取、自動重新抓取、自動處理載入和錯誤狀態。


為什麼需要 TanStack Query?

Server State vs Client State

當你用 AI 生成 React 程式碼時,你會看到兩種完全不同的「狀態」:

Client State(客戶端狀態) Server State(伺服器狀態)
資料來源 使用者操作產生 從 API / 資料庫抓回來
舉例 表單輸入、側邊欄開關、深色模式 使用者清單、文章列表、訂單資料
特性 你完全控制,改了就改了 別人也可能改,隨時可能過期
適合的工具 useState、Zustand TanStack Query

翻譯:表單打了什麼字、選單開或關——這些你自己管就好(useState)。但是「後端那邊的文章列表」可能隨時有人新增刪除,你不能只存一份就不管了——這就是 TanStack Query 要解決的問題。

不用 TanStack Query 時,你可能會看到這種寫法

function PostList() {
  const [posts, setPosts] = useState([]);       // 存資料
  const [loading, setLoading] = useState(true); // 存載入狀態
  const [error, setError] = useState(null);     // 存錯誤

  useEffect(() => {
    setLoading(true);
    fetch('/api/posts')
      .then(res => res.json())
      .then(data => setPosts(data))
      .catch(err => setError(err))
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error!</p>;
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
Code language: JavaScript (javascript)

問題在哪?

  • 每個元件都要自己寫 loading / error 狀態
  • 沒有快取:切換頁面再回來,又要重新抓
  • 沒有自動重新抓取:資料過期了也不知道
  • 多個元件用同一份資料時,各抓各的

用 TanStack Query 後的同樣功能

function PostList() {
  const { data: posts, isLoading, error } = useQuery({
    queryKey: ['posts'],
    queryFn: () => fetch('/api/posts').then(res => res.json()),
  });

  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error!</p>;
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
Code language: JavaScript (javascript)

翻譯:「去抓 /api/posts 的資料,用 'posts' 這個名字存起來(快取)。載入中、出錯、成功的狀態我都幫你管好了。」


QueryClientProvider:初始化設定

在你的 App 最外層,你會看到這段設定。這是 TanStack Query 的起點。

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5,  // 5 分鐘內算「新鮮」
      gcTime: 1000 * 60 * 30,    // 快取保留 30 分鐘
    },
  },
});

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

逐行翻譯

const queryClient = new QueryClient({       // 建立一個快取管理器
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5,             // 資料抓回來後 5 分鐘內不會重新抓
      gcTime: 1000 * 60 * 30,               // 沒人用的快取 30 分鐘後才清掉
    },
  },
});
Code language: JavaScript (javascript)
<QueryClientProvider client={queryClient}>   {/* 把快取管理器提供給整個 App */}
  <RouterOrYourApp />
</QueryClientProvider>
Code language: HTML, XML (xml)

知道就好staleTimegcTime 這裡設的是「全域預設值」。每個 useQuery 可以自己覆蓋。


useQuery:讀取資料的核心

最小範例

const { data, isLoading, error } = useQuery({
  queryKey: ['posts'],
  queryFn: () => fetch('/api/posts').then(res => res.json()),
});
Code language: JavaScript (javascript)

核心概念翻譯

你會看到 意思
queryKey: ['posts'] 這份資料的名字(用來識別和快取)
queryFn: () => ... 怎麼抓資料(回傳 Promise 的函式)
data 抓回來的資料
isLoading 第一次載入中(還沒有任何資料)
isFetching 正在抓資料(包含背景重新抓取)
error 如果出錯,這裡有錯誤資訊

isLoading vs isFetching——一定要看懂的差異

第一次進頁面:
  isLoading = true   ← 完全沒資料,顯示 Loading 畫面
  isFetching = true

抓到資料後切換到別的頁面,再切回來:
  isLoading = false   ← 有舊資料可以先顯示
  isFetching = true   ← 但背景正在重新抓取

背景抓取完成:
  isLoading = false
  isFetching = false  ← 資料已是最新
Code language: JavaScript (javascript)

翻譯isLoading 是「完全空白,什麼都沒有」。isFetching 是「正在更新,但可能有舊資料可以先看」。大部分情況下你用 isLoading 來決定要不要顯示 Loading 畫面。

staleTime 和 gcTime

這兩個設定控制了快取的行為,是 TanStack Query 最重要的概念之一。

staleTime(新鮮時間):
  「資料抓回來之後,多久之內不需要重新抓?」

  staleTime: 0          → 一回來就算過期(預設值)
  staleTime: 1000 * 601 分鐘內算新鮮
  staleTime: Infinity    → 永遠不過期(除非手動刷新)
Code language: JavaScript (javascript)
gcTime(垃圾回收時間):
  「沒有元件在用這份快取資料時,保留多久才清掉?」

  gcTime: 1000 * 60 * 55 分鐘後清掉(預設值)
  gcTime: Infinity         → 永遠不清
Code language: JavaScript (javascript)

翻譯staleTime 決定「多快要重新抓」,gcTime 決定「沒人看的時候多久才丟掉」。

queryKey 的設計——為什麼 key 結構很重要

queryKey 不只是個名字,它是一個陣列,TanStack Query 用它來決定「這是不是同一份資料」。

// 所有文章
useQuery({ queryKey: ['posts'], queryFn: fetchPosts });

// 特定文章(id = 5)
useQuery({ queryKey: ['posts', 5], queryFn: () => fetchPost(5) });

// 帶篩選條件的文章
useQuery({ queryKey: ['posts', { status: 'draft' }], queryFn: () => fetchPosts({ status: 'draft' }) });
Code language: JavaScript (javascript)

翻譯

  • ['posts'] — 「所有文章」這份資料
  • ['posts', 5] — 「第 5 篇文章」這份資料
  • ['posts', { status: 'draft' }] — 「草稿狀態的文章」這份資料

它們是三份不同的快取。queryKey 不同,資料就分開存。

queryKey 變化時的行為

function PostDetail({ postId }: { postId: number }) {
  const { data } = useQuery({
    queryKey: ['posts', postId],        // postId 變了 → queryKey 變了
    queryFn: () => fetchPost(postId),
  });
  // ...
}
Code language: JavaScript (javascript)

翻譯:當 postId 從 5 變成 10 時,TanStack Query 知道這是不同的資料,會自動去抓 fetchPost(10)。如果之前抓過 10 號文章的快取還在,就先顯示舊的,背景再確認是否需要更新。


快取行為:stale-while-revalidate

這是 TanStack Query 的核心策略,名字聽起來很複雜,但概念很簡單:

「先給你舊的看,背景偷偷去抓新的。」

完整流程圖

使用者第一次訪問「文章列表」頁面
  1. queryKey ['posts'] 沒有快取 → isLoading = true
  2. 呼叫 queryFn 抓資料
  3. 抓到了 → 存進快取,畫面顯示資料

使用者切到「關於」頁面(離開文章列表)
  4. 沒有元件在用 ['posts'] 了
  5. 但快取還在(要等 gcTime 到了才清)

使用者切回「文章列表」頁面
  6. 快取裡有資料 → 立刻顯示(不用等!)
  7. 但如果 staleTime 已過 → 背景重新抓取
  8. 新資料回來 → 自動更新畫面
Code language: JavaScript (javascript)

翻譯:使用者看到的是「瞬間顯示」(因為有快取),但資料是最新的(因為背景偷偷更新了)。

自動 refetch 的觸發時機

TanStack Query 會在這些時候自動重新抓取(前提是資料已經 stale):

時機 設定 預設
元件掛載時 refetchOnMount true
視窗重新聚焦 refetchOnWindowFocus true
網路重新連線 refetchOnReconnect true
固定間隔 refetchInterval false(停用)
// 範例:即時資料每 30 秒抓一次
useQuery({
  queryKey: ['notifications'],
  queryFn: fetchNotifications,
  refetchInterval: 1000 * 30,  // 每 30 秒自動抓
});
Code language: JavaScript (javascript)

翻譯:預設行為是「你切回瀏覽器視窗時,自動更新過期的資料」。這就是為什麼用了 TanStack Query 後,使用者 alt-tab 回來就能看到最新資料。


useMutation:寫入資料

useQuery 負責「讀」,useMutation 負責「寫」(新增、修改、刪除)。

最小範例

import { useMutation, useQueryClient } from '@tanstack/react-query';

function CreatePostButton() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (newPost) =>
      fetch('/api/posts', {
        method: 'POST',
        body: JSON.stringify(newPost),
        headers: { 'Content-Type': 'application/json' },
      }).then(res => res.json()),

    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });

  return (
    <button
      onClick={() => mutation.mutate({ title: 'New Post', body: 'Hello' })}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? 'Posting...' : 'Create Post'}
    </button>
  );
}
Code language: JavaScript (javascript)

逐行翻譯

const queryClient = useQueryClient();          // 取得快取管理器

const mutation = useMutation({
  mutationFn: (newPost) =>                     // 怎麼送資料到後端
    fetch('/api/posts', {
      method: 'POST',
      body: JSON.stringify(newPost),
    }).then(res => res.json()),

  onSuccess: () => {                           // 成功之後做什麼
    queryClient.invalidateQueries({
      queryKey: ['posts']                      // 告訴快取:['posts'] 的資料過期了,重新抓
    });
  },
});

mutation.mutate({ title: 'New Post' });        // 觸發這個 mutation
Code language: JavaScript (javascript)

mutation 的狀態

你會看到 意思
mutation.mutate(data) 執行這個操作
mutation.isPending 正在執行中
mutation.isError 執行失敗了
mutation.isSuccess 執行成功了
mutation.error 錯誤資訊
mutation.reset() 重設狀態

完整的錯誤處理

AI 常會生成包含 onErroronSettled 的寫法:

const mutation = useMutation({
  mutationFn: updatePost,
  onSuccess: (data) => {
    console.log('成功!回傳的資料:', data);
    queryClient.invalidateQueries({ queryKey: ['posts'] });
  },
  onError: (error) => {
    console.error('失敗了:', error.message);
  },
  onSettled: () => {
    console.log('不管成功或失敗都會執行');
  },
});
Code language: JavaScript (javascript)

翻譯

  • onSuccess:成功了,通常在這裡刷新快取
  • onError:失敗了,通常在這裡顯示錯誤提示
  • onSettled:不管成功失敗,通常在這裡關閉 loading 動畫

invalidateQueries——刷新快取的關鍵

// 刷新所有以 'posts' 開頭的快取
queryClient.invalidateQueries({ queryKey: ['posts'] });
// ↑ 這會讓 ['posts']、['posts', 5]、['posts', { status: 'draft' }] 全部重新抓取

// 只刷新特定的
queryClient.invalidateQueries({ queryKey: ['posts', 5] });
// ↑ 只有 ['posts', 5] 會重新抓取
Code language: JavaScript (javascript)

翻譯invalidateQueries 的意思是「告訴快取:這些資料過期了,下次有人要用就重新抓」。注意它是前綴匹配['posts'] 會匹配所有以 'posts' 開頭的 queryKey。


Optimistic Updates(樂觀更新)

概念

一般流程:按下「按讚」→ 等 API 回應 → 更新畫面 樂觀更新:按下「按讚」→ 立刻更新畫面 → 背景送 API → 如果失敗再回滾

翻譯:「先假設會成功,讓使用者立刻看到結果。如果後端說失敗,再復原。」

最小範例

const likeMutation = useMutation({
  mutationFn: (postId) => fetch(`/api/posts/${postId}/like`, { method: 'POST' }),

  onMutate: async (postId) => {
    // 1. 取消正在進行的查詢(避免覆蓋我們的樂觀更新)
    await queryClient.cancelQueries({ queryKey: ['posts', postId] });

    // 2. 保存舊資料(等等失敗時要用來回滾)
    const previousPost = queryClient.getQueryData(['posts', postId]);

    // 3. 樂觀更新:先在快取裡改掉
    queryClient.setQueryData(['posts', postId], (old) => ({
      ...old,
      likes: old.likes + 1,
    }));

    // 4. 把舊資料傳給 onError
    return { previousPost };
  },

  onError: (err, postId, context) => {
    // 失敗了!用舊資料回滾
    queryClient.setQueryData(['posts', postId], context.previousPost);
  },

  onSettled: (data, error, postId) => {
    // 不管成功失敗,都重新抓一次確保同步
    queryClient.invalidateQueries({ queryKey: ['posts', postId] });
  },
});
Code language: JavaScript (javascript)

流程翻譯

使用者按下「按讚」
  ↓
onMutate 執行:
  1. 取消正在進行的查詢
  2. 把目前的資料存起來(備份)
  3. 直接改快取裡的資料(likes + 1)→ 畫面立刻更新!
  ↓
mutationFn 執行:送 API 請求到後端
  ↓
如果成功 → onSuccess + onSettled → 重新抓取確保同步
如果失敗 → onError(用備份回滾)+ onSettled → 重新抓取

必看懂:Optimistic Updates 的核心就是三步——「備份、先改、失敗回滾」。


queryKey 設計模式:Query Key Factory

當專案變大,queryKey 到處散落會變得難以管理。你會看到 AI 生成類似這樣的「工廠函式」:

最小範例

// queryKeys.ts
export const postKeys = {
  all:    ['posts'] as const,
  lists:  () => [...postKeys.all, 'list'] as const,
  list:   (filters: object) => [...postKeys.lists(), filters] as const,
  details:() => [...postKeys.all, 'detail'] as const,
  detail: (id: number) => [...postKeys.details(), id] as const,
};
Code language: JavaScript (javascript)

使用方式

// 取得所有文章列表
useQuery({
  queryKey: postKeys.lists(),            // ['posts', 'list']
  queryFn: fetchPosts,
});

// 取得篩選後的文章
useQuery({
  queryKey: postKeys.list({ status: 'draft' }),  // ['posts', 'list', { status: 'draft' }]
  queryFn: () => fetchPosts({ status: 'draft' }),
});

// 取得單篇文章
useQuery({
  queryKey: postKeys.detail(5),          // ['posts', 'detail', 5]
  queryFn: () => fetchPost(5),
});

// 刷新所有文章相關的快取
queryClient.invalidateQueries({ queryKey: postKeys.all });
// ↑ 所有以 ['posts'] 開頭的都會被刷新
Code language: JavaScript (javascript)

為什麼這樣設計

postKeys.all          → ['posts']              → 刷新「所有文章相關」
postKeys.lists()      → ['posts', 'list']      → 刷新「所有列表」
postKeys.list({...})  → ['posts', 'list', ...] → 刷新「特定篩選的列表」
postKeys.details()    → ['posts', 'detail']    → 刷新「所有文章詳情」
postKeys.detail(5)    → ['posts', 'detail', 5] → 刷新「第 5 篇文章」
Code language: PHP (php)

翻譯:這是一個「從大到小」的樹狀結構。用 invalidateQueries 時,你可以選擇刷新整棵樹、某個分支、或一個節點。統一管理 key 的好處是不會打錯字,也容易搜尋「誰在用這個 key」。


Vibe Coder 檢查點

看到 AI 用 TanStack Query 寫的程式碼時,確認以下幾點:

useQuery 相關

  • [ ] queryKey 有包含所有影響查詢結果的變數嗎?(例如 id、篩選條件)
  • [ ] queryFn 有正確處理 HTTP 錯誤嗎?(fetch 不會自動 throw 4xx/5xx 錯誤)
  • [ ] staleTime 設定合理嗎?(太短 = 一直抓,太長 = 資料很舊)
  • [ ] 有區分 isLoadingisFetching 嗎?(初次載入 vs 背景更新)

useMutation 相關

  • [ ] onSuccess 有用 invalidateQueries 刷新相關的快取嗎?
  • [ ] 有處理 onError 嗎?(至少顯示錯誤訊息給使用者)
  • [ ] 按鈕有在 isPending 時 disabled 嗎?(避免重複送出)

Optimistic Updates 相關

  • [ ] onMutate 有備份舊資料嗎?
  • [ ] onError 有用備份回滾嗎?
  • [ ] onSettled 有重新抓取確保同步嗎?

queryKey 相關

  • [ ] 有用 Query Key Factory 統一管理嗎?(專案大的時候)
  • [ ] invalidateQueries 的範圍正確嗎?(太大 = 重抓太多,太小 = 漏更新)

常見問題速查

fetch 不會自動 throw 錯誤

// 有陷阱的寫法(404 不會進 error)
queryFn: () => fetch('/api/posts').then(res => res.json())

// 正確的寫法
queryFn: async () => {
  const res = await fetch('/api/posts');
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}
Code language: JavaScript (javascript)

翻譯fetch 在收到 404 或 500 時不會自己報錯。你要自己檢查 res.ok,不 OK 就手動丟出錯誤,TanStack Query 才能正確進入 error 狀態。

enabled 控制什麼時候才抓

const { data: user } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  enabled: !!userId,  // userId 存在時才抓
});
Code language: JavaScript (javascript)

翻譯enabled: false 時這個 query 完全不會執行。常用在「要等另一個資料回來才能抓」的場景。

select 只取部分資料

const { data: postTitles } = useQuery({
  queryKey: ['posts'],
  queryFn: fetchPosts,
  select: (posts) => posts.map(p => p.title),  // 只取標題
});
Code language: JavaScript (javascript)

翻譯select 讓你在快取的資料上做轉換。快取裡存的是完整資料,但元件只收到標題。好處是多個元件可以共用同一份快取但各取所需。


必看懂 vs 知道就好

必看懂(天天會遇到)

  • useQueryqueryKeyqueryFndataisLoading
  • useMutationmutationFnmutateonSuccess
  • invalidateQueries 刷新快取
  • staleTimegcTime 的意義
  • QueryClientProvider 初始化

知道就好(遇到再查)

  • useInfiniteQuery:無限捲動載入
  • useSuspense / Suspense 模式:搭配 React Suspense 使用
  • placeholderData:在資料載入前先顯示占位資料
  • useQueries:同時執行多個 query
  • Devtools:@tanstack/react-query-devtools 除錯工具

本篇小結

TanStack Query 的核心概念就是:

  1. useQuery 負責讀取——給一個 key 和抓資料的函式,它幫你管載入、錯誤、快取
  2. stale-while-revalidate 是快取策略——先顯示舊的,背景去抓新的
  3. useMutation 負責寫入——操作成功後用 invalidateQueries 刷新快取
  4. Optimistic Updates 讓 UX 更快——先改 UI 再送請求,失敗就回滾
  5. queryKey 是快取的身份證——結構設計好,管理和刷新都方便

下一篇我們會介紹如何把 TanStack Router 和 TanStack Query 整合在一起——用 loader 模式在路由層就開始抓資料,讓頁面切換更快。

進階測驗:TanStack Query 資料抓取與快取管理

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

1. 你在開發一個電商後台,商品列表頁需要顯示商品資料。使用者反映:每次從商品詳情頁切回列表頁時,都會看到一瞬間的 Loading 畫面,體驗很差。你應該怎麼調整? 情境題

// 目前的寫法 useQuery({ queryKey: [‘products’], queryFn: fetchProducts, staleTime: 0, // 預設值 gcTime: 1000 * 60, // 1 分鐘 });
  • A. 把 gcTime 設為 0,讓快取立刻清掉,強制每次重新抓取
  • B. 增加 staleTime(例如 5 分鐘),讓切回頁面時直接用快取資料,不觸發重新抓取
  • C. 在元件中改用 isFetching 來判斷是否顯示 Loading 畫面
  • D. 設定 refetchOnMount: false 完全關閉自動重新抓取

2. 你的應用有一個「刪除文章」功能。刪除成功後,文章列表和該文章的詳情快取都需要更新。以下哪種 invalidateQueries 的寫法最合適? 情境題

// Query Key Factory const postKeys = { all: [‘posts’] as const, lists: () => […postKeys.all, ‘list’] as const, list: (filters) => […postKeys.lists(), filters] as const, details: () => […postKeys.all, ‘detail’] as const, detail: (id) => […postKeys.details(), id] as const, };
  • A. queryClient.invalidateQueries({ queryKey: postKeys.all })
  • B. queryClient.invalidateQueries({ queryKey: postKeys.lists() })
  • C. queryClient.invalidateQueries({ queryKey: postKeys.detail(deletedId) })
  • D. 分別呼叫 invalidateQueries 兩次,一次傳 postKeys.lists(),一次傳 postKeys.detail(deletedId)

3. 你的應用有一個使用者資料頁面,需要先抓取目前登入使用者的 ID,再用這個 ID 去抓使用者的訂單列表。以下哪種寫法最正確? 情境題

  • A. 在一個 useQueryqueryFn 裡同時抓使用者和訂單
  • B. 用 useEffect 監聽使用者資料,有了之後再手動呼叫 fetch 抓訂單
  • C. 用兩個 useQuery,第二個設定 enabled: !!userId,等第一個的資料回來再執行
  • D. 用 useMutation 抓訂單,因為它依賴另一個請求的結果

4. 同事寫了一個 Optimistic Update,但使用者回報:按讚後畫面有閃一下,likes 先加 1 然後又跳回原本的數字,過了一秒再變成正確的數字。最可能的原因是什麼? 錯誤診斷

const likeMutation = useMutation({ mutationFn: (postId) => fetch(`/api/posts/${postId}/like`, { method: ‘POST’ }), onMutate: async (postId) => { const previousPost = queryClient.getQueryData([‘posts’, postId]); queryClient.setQueryData([‘posts’, postId], (old) => ({ …old, likes: old.likes + 1, })); return { previousPost }; }, onError: (err, postId, context) => { queryClient.setQueryData([‘posts’, postId], context.previousPost); }, onSettled: (data, error, postId) => { queryClient.invalidateQueries({ queryKey: [‘posts’, postId] }); }, });
  • A. onSettled 不應該呼叫 invalidateQueries,這導致多餘的重新抓取
  • B. onMutate 中少了 await queryClient.cancelQueries(),正在進行的舊查詢回來後覆蓋了樂觀更新的資料
  • C. mutationFn 沒有回傳 res.json(),導致 onSuccess 收不到新資料
  • D. queryKey 應該用 ['posts'] 而不是 ['posts', postId]

5. 你的同事寫了以下程式碼,但使用者回報:點擊不存在的文章(404)時,頁面一直卡在 Loading 狀態,不會顯示錯誤訊息。問題出在哪裡? 錯誤診斷

function PostDetail({ postId }) { const { data, isLoading, error } = useQuery({ queryKey: [‘posts’, postId], queryFn: () => fetch(`/api/posts/${postId}`).then(res => res.json()), }); if (isLoading) return <p>Loading…</p>; if (error) return <p>Error: {error.message}</p>; return <h1>{data.title}</h1>; }
  • A. queryKey 格式不對,應該只用 ['posts'] 不需要包含 postId
  • B. 應該用 useMutation 而不是 useQuery 來處理可能失敗的請求
  • C. fetch 在收到 404 時不會自動 throw 錯誤,queryFn 需要手動檢查 res.ok 並丟出錯誤
  • D. 缺少 enabled 設定,postId 可能還沒準備好就開始抓取

發佈留言

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