教學 進階

TanStack Query 5 交易資料獲取最佳實踐|React 伺服器狀態管理完整指南

Sentinel Team · 2026-03-04
TanStack Query 5 交易資料獲取最佳實踐|React 伺服器狀態管理完整指南

TanStack Query 5 交易資料獲取最佳實踐

快速導覽:本文深入探討 TanStack Query 5 在交易系統的完整應用,從基礎查詢到進階快取策略,提供加密貨幣交易資料獲取的權威指南。預計閱讀時間 14 分鐘。


為什麼交易系統需要 TanStack Query?

在自動化交易系統中,資料獲取是最複雜的挑戰之一。你需要同時處理:

傳統的 useEffect + fetch 模式在面對這些需求時迅速崩潰。根據 TanStack 官方文件,React Query 被設計為「伺服器狀態的非同步快取管理器」—— 這正是交易系統需要的。

交易系統的資料挑戰

| 資料類型 | 更新頻率 | 快取策略 | 挑戰 |

|:---|:---:|:---|:---|

| 即時行情 | 1-10 次/秒 | 短暫快取(5s)| 高頻更新效能 |

| 歷史 K 線 | 每分鐘 | 長期快取(1h)| 資料量大 |

| 用戶持倉 | 即時 | 即時失效 | 一致性要求 |

| 交易記錄 | 新增時 | 分頁快取 | 分頁同步 |

| 策略列表 | 每小時 | 背景更新 | 過期資料風險 |


TanStack Query 5 核心概念

伺服器狀態 vs 客戶端狀態

┌─────────────────────────────────────────────────────────┐
│                    應用程式狀態                          │
├─────────────────────────┬───────────────────────────────┤
│      客戶端狀態          │         伺服器狀態             │
│  (Zustand/Redux)        │      (TanStack Query)         │
├─────────────────────────┼───────────────────────────────┤
│ • UI 開關狀態           │ • API 回應資料                │
│ • 表單輸入值            │ • 快取的生命週期管理           │
│ • 動畫狀態              │ • 背景更新與重新驗證           │
│ • 主題設定              │ • 錯誤重試與取消請求           │
│                         │ • 分頁與無限滾動              │
└─────────────────────────┴───────────────────────────────┘

關鍵洞察:交易系統應該將即時價格以外的所有伺服器資料交給 TanStack Query 管理,UI 狀態則用 Zustand 處理。

Query Key 的設計哲學

Query Key 是 TanStack Query 的核心,決定了資料的識別與快取:

// ❌ 錯誤:過於簡單
const { data } = useQuery({
  queryKey: ['bots'],
  queryFn: fetchBots,
});

// ✅ 正確:包含所有依賴參數
const { data } = useQuery({
  queryKey: ['bots', { status: 'active', page: 1, limit: 20 }],
  queryFn: () => fetchBots({ status: 'active', page: 1, limit: 20 }),
});

交易系統的 Query Key 設計原則

// 即時價格 - 包含交易對與時間框架
['prices', 'BTC/USDT', '1m']  // 1 分鐘 K 線

// 機器人列表 - 包含篩選條件
['bots', { status: 'running', sort: 'pnl_desc' }]

// 交易記錄 - 包含分頁與時間範圍
['trades', { botId: '123', page: 1, from: '2024-01-01', to: '2024-01-31' }]

// 用戶資料 - 包含用戶 ID(確保切換用戶時快取失效)
['user', userId, 'profile']

交易系統實戰:Hooks 設計模式

基礎查詢:useBots

// hooks/useBots.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/api/client';

// Query Keys 集中管理
export const botKeys = {
  all: ['bots'] as const,
  lists: () => [...botKeys.all, 'list'] as const,
  list: (filters: BotFilters) => [...botKeys.lists(), filters] as const,
  details: () => [...botKeys.all, 'detail'] as const,
  detail: (id: string) => [...botKeys.details(), id] as const,
};

// 取得機器人列表
export function useBots(filters: BotFilters = {}) {
  return useQuery({
    queryKey: botKeys.list(filters),
    queryFn: () => api.bots.list(filters),
    staleTime: 1000 * 30, // 30 秒內視為新鮮
    gcTime: 1000 * 60 * 5, // 5 分鐘垃圾回收
  });
}

// 取得單一機器人
export function useBot(id: string) {
  return useQuery({
    queryKey: botKeys.detail(id),
    queryFn: () => api.bots.get(id),
    enabled: !!id, // ID 存在時才查詢
    staleTime: 1000 * 10, // 10 秒新鮮時間
  });
}

資料修改:useCreateBot

// hooks/useCreateBot.ts
export function useCreateBot() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: api.bots.create,
    
    // 樂觀更新
    onMutate: async (newBot) => {
      // 取消進行中的重新獲取
      await queryClient.cancelQueries({ queryKey: botKeys.lists() });
      
      // 儲存之前的值
      const previousBots = queryClient.getQueryData(botKeys.lists());
      
      // 樂觀更新快取
      queryClient.setQueryData(botKeys.lists(), (old) => 
        old ? [...old, { ...newBot, id: 'temp-id', status: 'creating' }] : old
      );
      
      return { previousBots };
    },
    
    // 錯誤時回滾
    onError: (err, newBot, context) => {
      queryClient.setQueryData(botKeys.lists(), context?.previousBots);
    },
    
    // 完成時重新獲取
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: botKeys.lists() });
    },
  });
}

想了解更多狀態管理?參考我們的 Zustand vs Redux 選型指南


即時資料:訂閱與輪詢策略

WebSocket 整合模式

// hooks/usePriceSubscription.ts
import { useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { wsManager } from '@/api/websocket';

export function usePriceSubscription(symbol: string) {
  const queryClient = useQueryClient();
  
  // 初始資料查詢
  const { data: price } = useQuery({
    queryKey: ['prices', symbol],
    queryFn: () => api.prices.getLatest(symbol),
    staleTime: Infinity, // WebSocket 負責更新
  });
  
  useEffect(() => {
    // 訂閱 WebSocket
    const unsubscribe = wsManager.subscribe(
      `price:${symbol}`,
      (newPrice) => {
        // 直接更新快取
        queryClient.setQueryData(
          ['prices', symbol],
          newPrice
        );
      }
    );
    
    return () => unsubscribe();
  }, [symbol, queryClient]);
  
  return price;
}

輪詢策略:useIntervalQuery

// hooks/useIntervalQuery.ts
export function useIntervalQuery(
  queryKey: string[],
  queryFn: () => Promise<T>,
  interval: number = 5000
) {
  return useQuery({
    queryKey,
    queryFn,
    refetchInterval: interval,
    refetchIntervalInBackground: false, // 背景不分頁
    refetchOnWindowFocus: true, // 回到視窗時重新獲取
  });
}

// 使用:每 5 秒更新持倉
function usePositions() {
  return useIntervalQuery(
    ['positions'],
    api.positions.getAll,
    5000
  );
}

快取策略:交易場景最佳實踐

staleTime 與 gcTime 的選擇

| 資料類型 | staleTime | gcTime | 理由 |

|:---|:---:|:---:|:---|

| 即時價格 | Infinity | 5s | WebSocket 更新,短暫保留 |

| 用戶資料 | 5min | 30min | 相對穩定 |

| 機器人列表 | 30s | 5min | 中等頻率更新 |

| 歷史 K 線 | 1h | 24h | 資料量大,長期快取 |

| 交易記錄 | 0 | 5min | 新增頻繁,即時重新獲取 |

分頁與無限滾動

// hooks/useTradeHistory.ts
export function useTradeHistory(botId: string) {
  return useInfiniteQuery({
    queryKey: ['trades', botId],
    queryFn: ({ pageParam = 1 }) => 
      api.trades.list({ botId, page: pageParam, limit: 50 }),
    getNextPageParam: (lastPage, pages) => {
      return lastPage.hasMore ? pages.length + 1 : undefined;
    },
    staleTime: 1000 * 60, // 1 分鐘
  });
}

// 組件中使用
function TradeHistory({ botId }: { botId: string }) {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = 
    useTradeHistory(botId);
  
  const trades = data?.pages.flatMap((page) => page.trades) ?? [];
  
  return (
    <div>
      {trades.map((trade) => (
        <TradeRow key={trade.id} trade={trade} />
      ))}
      
      {hasNextPage && (
        <button 
          onClick={() => fetchNextPage()}
          disabled={isFetchingNextPage}
        >
          {isFetchingNextPage ? '載入中...' : '載入更多'}
        </button>
      )}
    </div>
  );
}

錯誤處理與重試策略

交易系統的錯誤分類

// 自定義重試邏輯
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: (failureCount, error: any) => {
        // 不重試的錯誤
        if (error?.response?.status === 401) return false;
        if (error?.response?.status === 403) return false;
        if (error?.response?.status === 404) return false;
        
        // 最多重試 3 次
        return failureCount < 3;
      },
      retryDelay: (attemptIndex) => {
        // 指數退避
        return Math.min(1000 * 2 ** attemptIndex, 30000);
      },
    },
  },
});

錯誤邊界與降級

// hooks/useBotWithFallback.ts
export function useBotWithFallback(id: string) {
  const { data, error, isError } = useBot(id);
  
  // 錯誤時顯示本地快取或預設值
  const fallbackBot = useMemo(() => ({
    id,
    name: '載入中...',
    status: 'unknown',
  }), [id]);
  
  return {
    bot: isError ? fallbackBot : data,
    error,
    isError,
  };
}

效能優化:交易場景的特殊考量

選擇器優化

// ❌ 錯誤:每次返回新物件
function useActiveBots() {
  const { data } = useBots();
  return data?.filter((bot) => bot.status === 'active');
}

// ✅ 正確:使用選擇器快取
import { useMemo } from 'react';

function useActiveBots() {
  const { data } = useBots();
  
  return useMemo(() => 
    data?.filter((bot) => bot.status === 'active'),
    [data]
  );
}

大量資料的虛擬化

// 結合 react-window 與 TanStack Query
import { FixedSizeList } from 'react-window';

function LargeTradeList({ botId }: { botId: string }) {
  const { data } = useTradeHistory(botId);
  const trades = data?.pages.flatMap((p) => p.trades) ?? [];
  
  const Row = ({ index, style }: { index: number; style: any }) => (
    <div style={style}>
      <TradeRow trade={trades[index]} />
    </div>
  );
  
  return (
    <FixedSizeList
      height={600}
      itemCount={trades.length}
      itemSize={60}
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}

深入效能優化?參考 Vite 5 + PWA 交易應用程式效能優化


實戰案例:Sentinel Bot 的資料架構

Query Client 配置

// providers/query-provider.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // 交易系統預設配置
      staleTime: 1000 * 30, // 30 秒
      gcTime: 1000 * 60 * 5, // 5 分鐘
      retry: 3,
      retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
    },
    mutations: {
      retry: 1, // 修改操作只重試 1 次
    },
  },
});

自定義 Hooks 庫結構

src/hooks/
├── queries/           # 資料查詢 Hooks
│   ├── useBots.ts
│   ├── useStrategies.ts
│   ├── usePrices.ts
│   └── useTrades.ts
├── mutations/         # 資料修改 Hooks
│   ├── useCreateBot.ts
│   ├── useUpdateBot.ts
│   └── useDeleteBot.ts
├── subscriptions/     # 即時訂閱 Hooks
│   └── usePriceSubscription.ts
└── composite/         # 複合 Hooks
    └── useBotWithStats.ts

常見問題 FAQ

Q1: TanStack Query 5 與 SWR 的差別?

A: 兩者都是優秀的資料獲取庫:

交易系統推薦 TanStack Query,因為其分頁、無限滾動、離線支援更成熟。

Q2: 如何處理高頻更新的即時資料?

A: 推薦組合策略:

  1. WebSocket:即時推送更新
  2. TanStack Query:管理初始資料與狀態
  3. Zustand:極高頻資料(如逐筆成交)

參考我們的 WebSocket 即時交易監控開發指南

Q3: staleTime 與 cacheTime 的差別?(v5 為 gcTime)

A:

交易場景建議:即時資料設 staleTime: Infinity,讓 WebSocket 控制更新。

Q4: 如何測試 TanStack Query Hooks?

A: 使用 renderHookQueryClientProvider

import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const createWrapper = () => {
  const queryClient = new QueryClient();
  return ({ children }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
};

it('should fetch bots', async () => {
  const { result } = renderHook(() => useBots(), {
    wrapper: createWrapper(),
  });
  
  await waitFor(() => expect(result.current.isSuccess).toBe(true));
  expect(result.current.data).toHaveLength(3);
});

Q5: 多個組件訂閱相同 Query Key 會重複請求嗎?

A: 不會。TanStack Query 自動去重,相同 Query Key 只會發送一個請求,所有訂閱者共享結果。

Q6: 如何強制重新獲取資料?

A: 多種方式:

const queryClient = useQueryClient();

// 方式 1:標記為過期(下次使用時重新獲取)
queryClient.invalidateQueries({ queryKey: ['bots'] });

// 方式 2:立即重新獲取
queryClient.refetchQueries({ queryKey: ['bots'] });

// 方式 3:在 Hook 中使用
const { refetch } = useBots();
refetch();

Q7: 離線支援如何實作?

A: TanStack Query v5 內建離線支援:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      networkMode: 'offlineFirst', // 離線時使用快取
    },
    mutations: {
      networkMode: 'offlineFirst',
      retry: Infinity, // 離線時持續重試
    },
  },
});

Q8: 與 Zustand 如何分工?

A: 黃金法則:

// 組合使用範例
function TradingDashboard() {
  // 伺服器狀態
  const { data: bots } = useBots();
  
  // 客戶端狀態
  const selectedBotId = useUIStore((s) => s.selectedBotId);
  const setSelectedBot = useUIStore((s) => s.setSelectedBot);
  
  return (
    <div>
      {bots?.map((bot) => (
        <BotCard
          key={bot.id}
          bot={bot}
          isSelected={bot.id === selectedBotId}
          onClick={() => setSelectedBot(bot.id)}
        />
      ))}
    </div>
  );
}

結論與最佳實踐總結

TanStack Query 5 是交易系統資料獲取的最佳選擇,它解決了:

立即行動檢查清單


延伸閱讀


作者:Sentinel Team

最後更新:2026-03-04

技術驗證:本文基於 Sentinel Bot 生產環境實戰經驗


正在優化交易系統的資料架構?立即體驗 Sentinel Bot 的 TanStack Query 驅動介面,或下載我們的 Hooks 模板快速開始。

免費試用 Sentinel Bot | 下載 Hooks 模板 | 技術諮詢


相關文章

同系列延伸閱讀

跨系列推薦