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:
- Historical data caching and pagination
- Real-time market data subscription and updates
- User operation optimistic updates
- Network instability error retries
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:
- TanStack Query: More complete features, larger community, active development
- SWR: Smaller size, from Vercel, good Next.js integration
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:
- WebSocket: Real-time push updates
- TanStack Query: Manage initial data and state
- 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:
- staleTime: Time data is considered "fresh", no refetching during this period
- gcTime: Time before data is removed from cache (garbage collection)
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:
- TanStack Query: Server state (API data)
- Zustand: Client state (UI, forms, theme)
// 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:
- ✅ Cache management
- ✅ Background updates
- ✅ Error retries
- ✅ Pagination infinite scroll
- ✅ Optimistic updates
Immediate Action Checklist
- [ ] Review existing data fetching logic
- [ ] Design reasonable Query Key structure
- [ ] Set appropriate staleTime/gcTime
- [ ] Implement error handling and retry strategy
- [ ] Establish custom Hooks library
- [ ] Setup performance monitoring
Extended Reading:
- TanStack Query Official Documentation
- React Query Performance Best Practices
- Web Vitals and Data Fetching
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
- React 18 Trading Interface - Frontend framework integration
- WebSocket Real-time Monitoring - Real-time data sync
- TypeScript 5 Type Safety - Type-safe data fetching
Cross-series Recommendations
- Quantitative Trading - Data-driven strategies