iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
Vibe Coding

Vibe Mode 開啟:30 天用 AI 打造網頁,邊做邊學 JavaScript系列 第 15 篇

# Day 15 - 前後端 API 串接:讓 AI 寫 Client-side 資料獲取(TanStack Query / SWR)

  • 分享至 

  • xImage
  •  

「資料不只是抓過來,還要抓得優雅」

昨天我們搞定了複雜的交易紀錄表單(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)。

Human vs. AI:提示詞與協同開發

在以往,寫 useQuery 或 useMutation 需要定義型別、撰寫 fetcher 函式,還要處理 queryKey 的管理。現在我們交給 AI,但關鍵在於上下文的精準度。

給 AI 的指令(Prompt)

我們正在開發 Finance Tracker 前端。
請幫我建立一個自訂 Hook useTransactions(路徑:src/hooks/useTransactions.ts),使用 TanStack Query v5。

要求:

  1. 使用 fetcher 呼叫 /api/transactions GET 介面取得清單。
  2. 包含一個 useAddTransaction Mutation,用 POST 呼叫同一個 API 帶入新資料。
  3. 當成功新增資料(onSuccess)時,必須自動讓 ['transactions'] 的 query key 失效並重新抓取(invalidateQueries)。
  4. 所有的資料與參數必須擁有嚴格的 TypeScript 型別定義。

產出的原始結果

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']);
    },
  });
};

翻車與除錯過程 (Debug Experience)

看似毫無瑕疵的程式碼,一把 Hook 接入前端元件,控制台立馬爆出紅字:

Uncaught TypeError: queryClient.invalidateQueries is not a function (或語法錯誤警告)

💥 AI 哪裡搞砸了?

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 拒絕狀態。

修正與引導(Human Decision)

我沒有自己手動改,而是把錯誤直接丟回給 Cursor,進行第二輪精準校正:

修正 Prompt:
「你在 invalidateQueries 使用了 TanStack Query v4 的舊語法。請改為 v5 的物件格式 { queryKey: [...] }。另外,請在 mutationFn 的 fetch 增加 if (!res.ok) 判斷,拋出包含伺服器錯誤訊息的 Error。」

最終修正後的版本 (Refactored Code)

經過調整後,我們獲得了具備高品質與強健性的前端 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'] });
    },
  });
};

JavaScript 筆記

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 圖表元件~~~


上一篇
# Day 14:複雜 Form 表單開發:AI 處理表單驗證、動態欄位與狀態控制
系列文
Vibe Mode 開啟:30 天用 AI 打造網頁,邊做邊學 JavaScript 共 15 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言