「資料不只是抓過來,還要抓得優雅」
昨天我們搞定了複雜的交易紀錄表單(Form),今天我們要讓前端正式與後端 API 溝通。告別傳統混亂的 useEffect + fetch,我們邀請當今前端資料獲取(Data Fetching)的神兵利器 —— TanStack Query (React Query) 上場!
在我們的 「AI 智慧個人財務追蹤器 (Smart Finance Tracker)」 中:
使用 TanStack Query (v5) 封裝 API 請求,實作交易紀錄列表的自動抓取、快取與重新發起請求(Re-validation)。
透過 Mutation 實現新增交易後「不重新整理頁面,自動更新清單」的無縫體驗。
掌握 Vibe Coding 下如何引導 AI 處理競態條件(Race Conditions)與載入狀態(Loading / Error States)。
在以往,寫 useQuery 或 useMutation 需要定義型別、撰寫 fetcher 函式,還要處理 queryKey 的管理。現在我們交給 AI,但關鍵在於上下文的精準度。
我們正在開發 Finance Tracker 前端。
請幫我建立一個自訂 Hook useTransactions(路徑:src/hooks/useTransactions.ts),使用 TanStack Query v5。
要求:
/api/transactions GET 介面取得清單。useAddTransaction Mutation,用 POST 呼叫同一個 API 帶入新資料。['transactions'] 的 query key 失效並重新抓取(invalidateQueries)。AI 的反應非常迅速,幾秒內就吐出了一段看似完美的程式碼:
// src/hooks/useTransactions.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
interface Transaction {
id: string;
amount: number;
category: string;
note: string;
}
const fetchTransactions = async (): Promise<Transaction[]> => {
const res = await fetch('/api/transactions');
if (!res.ok) throw new Error('Network response was not ok');
return res.json();
};
export const useTransactions = () => {
return useQuery({
queryKey: ['transactions'],
queryFn: fetchTransactions,
});
};
export const useAddTransaction = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (newTx: Omit<Transaction, 'id'>) => {
const res = await fetch('/api/transactions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTx),
});
return res.json();
},
onSuccess: () => {
// ⚠️ 這裡出事了!
queryClient.invalidateQueries(['transactions']);
},
});
};
看似毫無瑕疵的程式碼,一把 Hook 接入前端元件,控制台立馬爆出紅字:
Uncaught TypeError: queryClient.invalidateQueries is not a function (或語法錯誤警告)
API 版本混淆(v4 vs v5):AI 雖然被告知使用 TanStack Query v5,但它在 invalidateQueries 的語法中使用了 v4 的陣列傳參方式 queryClient.invalidateQueries(['transactions'])。在 v5 中,必須傳入一個物件:queryClient.invalidateQueries({ queryKey: ['transactions'] })!
缺乏 Error Boundary / Toast 整合:如果 POST 失敗,res.json() 依然可能回傳 400/500 的錯誤訊息,但 AI 沒檢查 res.ok 就直接解析,導致前端陷入未捕獲的 Promise 拒絕狀態。
我沒有自己手動改,而是把錯誤直接丟回給 Cursor,進行第二輪精準校正:
修正 Prompt:
「你在 invalidateQueries 使用了 TanStack Query v4 的舊語法。請改為 v5 的物件格式 { queryKey: [...] }。另外,請在 mutationFn 的 fetch 增加 if (!res.ok) 判斷,拋出包含伺服器錯誤訊息的 Error。」
經過調整後,我們獲得了具備高品質與強健性的前端 Data Fetching 模組:
// src/hooks/useTransactions.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export interface Transaction {
id: string;
amount: number;
category: string;
note: string;
createdAt: string;
}
export type NewTransactionInput = Omit<Transaction, 'id' | 'createdAt'>;
const fetchTransactions = async (): Promise<Transaction[]> => {
const res = await fetch('/api/transactions');
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new Error(errorData.message || '無法取得交易資料');
}
return res.json();
};
export const useTransactions = () => {
return useQuery({
queryKey: ['transactions'],
queryFn: fetchTransactions,
staleTime: 1000 * 60 * 5, // 5分鐘內資料視為最新,不重複發送請求
});
};
export const useAddTransaction = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (newTx: NewTransactionInput) => {
const res = await fetch('/api/transactions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newTx),
});
if (!res.ok) {
const errorData = await res.json().catch(() => ({}));
throw new Error(errorData.message || '新增交易失敗');
}
return res.json();
},
onSuccess: () => {
// 正確 TanStack Query v5 語法
queryClient.invalidateQueries({ queryKey: ['transactions'] });
},
});
};
Stale-While-Revalidate 哲學:
傳統的 useEffect 每次 Component 重新渲染就可能重新發送 API,極易造成瀑布式請求(Waterfall)或重複抓取。使用 TanStack Query 透過 queryKey 機制,能自動幫我們處理快取、背景更新(Background Refetching)以及去重(Deduplication)。
AI 的盲點在於套件版本迭代:
前端生態系更新極快(React Query v3 ➔ v4 ➔ v5 破壞性變更不少)。當你發現 AI 寫出不可執行的程式碼時,優先檢查 API 版本,並在 Prompt 中明確提示版本差異,是 Vibe Coding 的重要基本功。
今天我們成功使用 AI 在幾分鐘內建立了堅固的前端 Client-side 請求層。明日 Day 16,我們將把今天抓到的動態交易資料,串接到 Recharts 圖表元件~~~