Strategy Intermediate

TanStack Query 5 Trading Data Fetching Best Practices

Sentinel Team · 2026-03-09
TanStack Query 5 Trading Data Fetching Best Practices

TanStack Query 5 Trading Data Fetching Best Practices

Quick Navigation: This article deeply explores the complete application of TanStack Query 5 in trading systems, from basic queries to advanced caching strategies, providing an authoritative guide for cryptocurrency trading data fetching. Estimated reading time: 14 minutes.


Why Do Trading Systems Need TanStack Query?

In automated trading systems, data fetching is one of the most complex challenges. You need to handle simultaneously:

Traditional useEffect + fetch patterns quickly collapse when facing these requirements. According to TanStack Official Documentation, React Query is designed as an "asynchronous cache manager for server state" — exactly what trading systems need.

Trading System Data Challenges

| Data Type | Update Frequency | Caching Strategy | Challenge |

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

| Real-time Prices | 1-10 times/sec | Short cache (5s) | High-frequency update performance |

| Historical K-lines | Every minute | Long-term cache (1h) | Large data volume |

| User Positions | Real-time | Instant invalidation | Consistency requirements |

| Trade Records | On addition | Pagination cache | Pagination sync |

| Strategy List | Hourly | Background update | Stale data risk |


TanStack Query 5 Core Concepts

Server State vs Client State

┌─────────────────────────────────────────────────────────┐
│                    Application State                     │
├─────────────────────────┬───────────────────────────────┤
│      Client State       │         Server State          │
│  (Zustand/Redux)        │      (TanStack Query)         │
├─────────────────────────┼───────────────────────────────┤
│ • UI toggle state       │ • API response data           │
│ • Form input values     │ • Cache lifecycle management  │
│ • Animation states      │ • Background update & revalidation |
│ • Theme settings        │ • Error retry & request cancellation |
│                         │ • Pagination & infinite scroll |
└─────────────────────────┴───────────────────────────────┘

Key Insight: Trading systems should hand over all server data except real-time prices to TanStack Query management, UI states handled by Zustand.

Query Key Design Philosophy

Query Key is the core of TanStack Query, determining data identification and caching:

// ❌ Wrong: Too simple
const { data } = useQuery({
  queryKey: ['bots'],
  queryFn: fetchBots,
});

// ✅ Correct: Include all dependency parameters
const { data } = useQuery({
  queryKey: ['bots', { status: 'active', page: 1, limit: 20 }],
  queryFn: () => fetchBots({ status: 'active', page: 1, limit: 20 }),
});

Trading System Query Key Design Principles:

// Real-time prices - Include trading pair and timeframe
['prices', 'BTC/USDT', '1m']  // 1-minute K-line

// Robot list - Include filter conditions
['bots', { status: 'running', sort: 'pnl_desc' }]

// Trade records - Include pagination and time range
['trades', { botId: '123', page: 1, from: '2024-01-01', to: '2024-01-31' }]

// User data - Include user ID (ensure cache invalidation when switching users)
['user', userId, 'profile']

Trading System in Practice: Hooks Design Patterns

Basic Query: useBots

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

// Query Keys centralized management
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,
};

// Get robot list
export function useBots(filters: BotFilters = {}) {
  return useQuery({
    queryKey: botKeys.list(filters),
    queryFn: () => api.bots.list(filters),
    staleTime: 1000 * 30, // Considered fresh within 30 seconds
    gcTime: 1000 * 60 * 5, // 5 minutes garbage collection
  });
}

// Get single robot
export function useBot(id: string) {
  return useQuery({
    queryKey: botKeys.detail(id),
    queryFn: () => api.bots.get(id),
    enabled: !!id, // Only query when ID exists
    staleTime: 1000 * 10, // 10 seconds fresh time
  });
}

Data Modification: useCreateBot

// hooks/useCreateBot.ts
export function useCreateBot() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: api.bots.create,
    
    // Optimistic update
    onMutate: async (newBot) => {
      // Cancel in-progress refetches
      await queryClient.cancelQueries({ queryKey: botKeys.lists() });
      
      // Save previous value
      const previousBots = queryClient.getQueryData(botKeys.lists());
      
      // Optimistically update cache
      queryClient.setQueryData(botKeys.lists(), (old) => 
        old ? [...old, { ...newBot, id: 'temp-id', status: 'creating' }] : old
      );
      
      return { previousBots };
    },
    
    // Rollback on error
    onError: (err, newBot, context) => {
      queryClient.setQueryData(botKeys.lists(), context?.previousBots);
    },
    
    // Refetch on completion
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: botKeys.lists() });
    },
  });
}

Want to learn more about state management? Check out our Zustand vs Redux Selection Guide.


Real-time Data: Subscription and Polling Strategies

WebSocket Integration Pattern

// 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();
  
  // Initial data query
  const { data: price } = useQuery({
    queryKey: ['prices', symbol],
    queryFn: () => api.prices.getLatest(symbol),
    staleTime: Infinity, // WebSocket handles updates
  });
  
  useEffect(() => {
    // Subscribe to WebSocket
    const unsubscribe = wsManager.subscribe(
      `price:${symbol}`,
      (newPrice) => {
        // Directly update cache
        queryClient.setQueryData(
          ['prices', symbol],
          newPrice
        );
      }
    );
    
    return () => unsubscribe();
  }, [symbol, queryClient]);
  
  return price;
}

Polling Strategy: useIntervalQuery

// hooks/useIntervalQuery.ts
export function useIntervalQuery(
  queryKey: string[],
  queryFn: () => Promise<T>,
  interval: number = 5000
) {
  return useQuery({
    queryKey,
    queryFn,
    refetchInterval: interval,
    refetchIntervalInBackground: false, // No background polling
    refetchOnWindowFocus: true, // Refetch when returning to window
  });
}

// Usage: Update positions every 5 seconds
function usePositions() {
  return useIntervalQuery(
    ['positions'],
    api.positions.getAll,
    5000
  );
}

Caching Strategy: Best Practices for Trading Scenarios

staleTime vs gcTime Selection

| Data Type | staleTime | gcTime | Reason |

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

| Real-time Prices | Infinity | 5s | WebSocket updates, short retention |

| User Data | 5min | 30min | Relatively stable |

| Robot List | 30s | 5min | Medium frequency updates |

| Historical K-lines | 1h | 24h | Large data volume, long-term cache |

| Trade Records | 0 | 5min | Frequent additions, instant refetch |

Pagination and Infinite Scroll

// 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 minute
  });
}

// Usage in component
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 ? 'Loading...' : 'Load More'}
        </button>
      )}
    </div>
  );
}

Error Handling and Retry Strategy

Trading System Error Classification

// Custom retry logic
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: (failureCount, error: any) => {
        // Don't retry these errors
        if (error?.response?.status === 401) return false;
        if (error?.response?.status === 403) return false;
        if (error?.response?.status === 404) return false;
        
        // Retry up to 3 times
        return failureCount < 3;
      },
      retryDelay: (attemptIndex) => {
        // Exponential backoff
        return Math.min(1000 * 2 ** attemptIndex, 30000);
      },
    },
  },
});

Error Boundary and Fallback

// hooks/useBotWithFallback.ts
export function useBotWithFallback(id: string) {
  const { data, error, isError } = useBot(id);
  
  // Show local cache or default values on error
  const fallbackBot = useMemo(() => ({
    id,
    name: 'Loading...',
    status: 'unknown',
  }), [id]);
  
  return {
    bot: isError ? fallbackBot : data,
    error,
    isError,
  };
}

Performance Optimization: Special Considerations for Trading Scenarios

Selector Optimization

// ❌ Wrong: Returns new object each time
function useActiveBots() {
  const { data } = useBots();
  return data?.filter((bot) => bot.status === 'active');
}

// ✅ Correct: Use selector caching
import { useMemo } from 'react';

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

Large Data Virtualization

// Combine react-window with 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>
  );
}

Deep dive into performance optimization? Check out Vite 5 + PWA Trading Application Performance Optimization.


Real-world Case: Sentinel Bot's Data Architecture

Query Client Configuration

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // Trading system default configuration
      staleTime: 1000 * 30, // 30 seconds
      gcTime: 1000 * 60 * 5, // 5 minutes
      retry: 3,
      retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
    },
    mutations: {
      retry: 1, // Modify operations only retry 1 time
    },
  },
});

Custom Hooks Library Structure

src/hooks/
├── queries/           # Data query Hooks
│   ├── useBots.ts
│   ├── useStrategies.ts
│   ├── usePrices.ts
│   └── useTrades.ts
├── mutations/         # Data modification Hooks
│   ├── useCreateBot.ts
│   ├── useUpdateBot.ts
│   └── useDeleteBot.ts
├── subscriptions/     # Real-time subscription Hooks
│   └── usePriceSubscription.ts
└── composite/         # Composite Hooks
    └── useBotWithStats.ts

Frequently Asked Questions FAQ

Q1: Difference between TanStack Query 5 and SWR?

A: Both are excellent data fetching libraries:

Trading systems recommend TanStack Query for its pagination, infinite scroll, offline support maturity.

Q2: How to handle high-frequency real-time data updates?

A: Recommended combination strategy:

  1. WebSocket: Real-time push updates
  2. TanStack Query: Manage initial data and state
  3. Zustand: Extremely high-frequency data (like tick-by-tick trades)

Refer to our WebSocket Real-time Trading Monitor Guide.

Q3: Difference between staleTime and cacheTime? (v5 is gcTime)

A:

Trading scenario recommendation: Set real-time data to staleTime: Infinity, let WebSocket control updates.

Q4: How to test TanStack Query Hooks?

A: Use renderHook with 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: Will multiple components subscribing to the same Query Key make duplicate requests?

A: No. TanStack Query automatically deduplicates, same Query Key only sends one request, all subscribers share results.

Q6: How to force refetch data?

A: Multiple ways:

const queryClient = useQueryClient();

// Method 1: Mark as stale (refetch on next use)
queryClient.invalidateQueries({ queryKey: ['bots'] });

// Method 2: Immediate refetch
queryClient.refetchQueries({ queryKey: ['bots'] });

// Method 3: Use in Hook
const { refetch } = useBots();
refetch();

Q7: How to implement offline support?

A: TanStack Query v5 has built-in offline support:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      networkMode: 'offlineFirst', // Use cache when offline
    },
    mutations: {
      networkMode: 'offlineFirst',
      retry: Infinity, // Keep retrying when offline
    },
  },
});

Q8: How to divide work with Zustand?

A: Golden rule:

// Combined usage example
function TradingDashboard() {
  // Server state
  const { data: bots } = useBots();
  
  // Client state
  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>
  );
}

Conclusion and Best Practices Summary

TanStack Query 5 is the best choice for trading system data fetching, it solves:

Immediate Action Checklist


Extended Reading:


Author: Sentinel Team

Last Updated: 2026-03-04

Technical Verification: Based on Sentinel Bot production environment practical experience


Optimizing your trading system data architecture? Experience Sentinel Bot's TanStack Query driven interface now, or download our Hooks template to get started quickly.

Free Trial Sentinel Bot | Download Hooks Template | Technical Consultation


Related Articles

Same Series Extended Reading

Cross-series Recommendations