教程 进阶

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 模板 | 技术咨询


相关文章

同系列延伸阅读

跨系列推荐