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:功能更完整,社群更大,開發活躍
- SWR:體積更小,Vercel 出品,Next.js 整合好
交易系統推薦 TanStack Query,因為其分頁、無限滾動、離線支援更成熟。
Q2: 如何處理高頻更新的即時資料?
A: 推薦組合策略:
- WebSocket:即時推送更新
- TanStack Query:管理初始資料與狀態
- Zustand:極高頻資料(如逐筆成交)
參考我們的 WebSocket 即時交易監控開發指南。
Q3: staleTime 與 cacheTime 的差別?(v5 為 gcTime)
A:
- staleTime:資料視為「新鮮」的時間,期間不重新獲取
- gcTime:資料從快取移除的時間(垃圾回收)
交易場景建議:即時資料設 staleTime: Infinity,讓 WebSocket 控制更新。
Q4: 如何測試 TanStack Query Hooks?
A: 使用 renderHook 與 QueryClientProvider:
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: 黃金法則:
- TanStack Query:伺服器狀態(API 資料)
- Zustand:客戶端狀態(UI、表單、主題)
// 組合使用範例
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 是交易系統資料獲取的最佳選擇,它解決了:
- ✅ 快取管理
- ✅ 背景更新
- ✅ 錯誤重試
- ✅ 分頁無限滾動
- ✅ 樂觀更新
立即行動檢查清單
- [ ] 審視現有資料獲取邏輯
- [ ] 設計合理的 Query Key 結構
- [ ] 設定適當的 staleTime/gcTime
- [ ] 實作錯誤處理與重試策略
- [ ] 建立自定義 Hooks 庫
- [ ] 設定效能監控
延伸閱讀:
作者:Sentinel Team
最後更新:2026-03-04
技術驗證:本文基於 Sentinel Bot 生產環境實戰經驗
正在優化交易系統的資料架構?立即體驗 Sentinel Bot 的 TanStack Query 驅動介面,或下載我們的 Hooks 模板快速開始。
免費試用 Sentinel Bot | 下載 Hooks 模板 | 技術諮詢
相關文章
同系列延伸閱讀
- React 18 交易介面 - 前端框架整合
- WebSocket 即時監控 - 即時資料同步
- TypeScript 5 型別安全 - 型別安全資料獲取
跨系列推薦
- 量化交易 - 資料驅動策略