5.2 KiB
5.2 KiB
TanStack Query (React Query)
Documentation for server state management using TanStack Query v5.
Overview
TanStack Query handles all server-side data (games, playlists, users) with automatic caching, background refetching, and request deduplication.
Version: 5.62.7
Configuration
Global query client configuration in main.tsx:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
refetchOnWindowFocus: false,
retry: 1
}
}
});
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
Custom Hooks Pattern
All API calls are wrapped in custom hooks:
// hooks/useGames.ts
import { useQuery } from '@tanstack/react-query';
import { gamesApi } from '@/lib/api';
export function useGames(filters: GameFilters) {
return useQuery({
queryKey: ['games', filters],
queryFn: () => gamesApi.search(filters),
staleTime: 5 * 60 * 1000,
});
}
Query Keys
Query keys determine caching and invalidation:
// Simple key
queryKey: ['games'];
// With parameters
queryKey: ['games', filters];
// Nested resources
queryKey: ['game', gameId, 'related'];
// Complex keys
queryKey: ['users', { page, limit, search }];
Mutations
For data modifications (create, update, delete):
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useCreatePlaylist() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (playlist) => playlistsApi.create(playlist),
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['playlists'] });
toast.success('Playlist created');
},
onError: (error) => {
toast.error('Failed to create playlist');
},
});
}
Optimistic Updates
Update UI immediately before server response:
export function useToggleFavorite() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (gameId) => favoritesApi.toggle(gameId),
onMutate: async (gameId) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['favorites'] });
// Snapshot previous value
const previousFavorites = queryClient.getQueryData(['favorites']);
// Optimistically update
queryClient.setQueryData(['favorites'], (old) => {
// Update logic here
});
return { previousFavorites };
},
onError: (err, gameId, context) => {
// Rollback on error
queryClient.setQueryData(['favorites'], context.previousFavorites);
},
onSettled: () => {
// Refetch after mutation
queryClient.invalidateQueries({ queryKey: ['favorites'] });
},
});
}
Stale Time Strategy
Different data has different stale times:
- Games: 5 minutes (rarely change)
- User data: 1 minute (may change frequently)
- Statistics: 30 seconds (real-time updates)
// Long stale time for stable data
useQuery({
queryKey: ['games', id],
queryFn: () => gamesApi.getById(id),
staleTime: 5 * 60 * 1000, // 5 minutes
});
// Short stale time for dynamic data
useQuery({
queryKey: ['stats'],
queryFn: () => statsApi.get(),
staleTime: 30 * 1000, // 30 seconds
});
Enabled Queries
Conditional query execution:
useQuery({
queryKey: ['game', id],
queryFn: () => gamesApi.getById(id),
enabled: !!id, // Only run if id exists
});
Conditional Refetch Intervals
Poll with dynamic intervals based on data state:
// Example: Poll for game download completion
useQuery({
queryKey: ['game', id, 'launch'],
queryFn: () => gamesApi.getGameLaunchData(id),
// Poll every 2 seconds if downloading, stop when done
refetchInterval: (query) => query.state.data?.downloading ? 2000 : false,
});
// This replaces manual polling logic with declarative TanStack Query patterns
Use Cases:
- Download progress polling (poll while
downloading: true) - Live status updates (poll while in progress, stop when complete)
- Real-time data that becomes stale at specific times
- User activity tracking
Benefits:
- Automatic cleanup when condition becomes false
- Respects query cache and stale time
- Deduplicates requests via query client
- Integrates with DevTools
Infinite Queries
For pagination:
useInfiniteQuery({
queryKey: ['games', filters],
queryFn: ({ pageParam = 1 }) =>
gamesApi.search({ ...filters, page: pageParam }),
getNextPageParam: (lastPage) => {
return lastPage.page < lastPage.totalPages ? lastPage.page + 1 : undefined;
},
});
DevTools
Enable React Query DevTools for debugging:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>