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 类型安全 - 类型安全数据获取
跨系列推荐
- 量化交易 - 数据驱动策略