TanStack Query Guide
Deckly uses TanStack Query (React Query) v5 to manage all asynchronous server state. This replaces manual useEffect fetching and manual in-memory caching.
Core Strategy
- Pure Services: Service files (
src/services/) should only contain pure data-fetching functions. They should not manage local state or caches. - Custom Hooks: All data-fetching logic is encapsulated in custom hooks within
src/hooks/(e.g.,useDecks,useProfile,useLibrary). - Centralized Keys: Query keys are managed at the hook level to ensure consistency during invalidation.
- Predictive Prefetching: We use intent-based prefetching (hover) and global background preloading (idle) to eliminate perceived network latency during navigation.
Prefetching Strategy
Deckly implements a multi-layered prefetching system to ensure "zero-latency" transitions:
Layer 1: Idle-Time Preloading
In Home.tsx, we utilize queryClient.prefetchQuery to warm up the cache for primary data indices (Decks, Rooms, Library) once the dashboard is stable. This ensures that the first click into any repository is instant.
Layer 2: Intent-Based (Hover)
Components that trigger navigation (e.g. Sidebar.tsx, DataRoomCard.tsx, DocumentRow.tsx) implement onMouseEnter handlers.
- Data Hook: Calls
queryClient.prefetchQueryfor the specific resource metadata. - Module Hook: Triggers a dynamic
import()of the destination page bundle to ensure the JS is already in memory.
Layer 3: Deferred Analytics
To preserve database throughput, we explicitly do not prefetch heavy analytics data. Historical trends and visitor signals are loaded strictly on-demand when the user enters the detail view.
Key Patterns
1. Data Fetching (useQuery)
Used for all GET requests. Always provide a clear queryKey.
export function useDeckStats(deckId: string) {
return useQuery({
queryKey: ["deck-stats", deckId],
queryFn: () => analyticsService.getDeckStats(deckId),
staleTime: 0, // Fresh data for analytics
refetchInterval: 45000, // 45s live refresh
refetchOnWindowFocus: true, // Sync on focus
});
}2. Mutations & Invalidation (useMutation)
Used for POST/PUT/DELETE requests. Always invalidate related keys on success.
const mutation = useMutation({
mutationFn: (newDeck) => deckService.uploadDeck(newDeck.file, newDeck.data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["decks"] });
},
});3. Optimistic Updates
For a high-end feel, we update the UI before the server confirms. This is used in NotesSidebar.tsx.
- Cancel outgoing queries for that key.
- Snapshot the current data.
- Manually update the cache.
- Rollback on error.
Current repo note:
- viewer/library query keys were recently standardized after optimistic updates drifted from their read keys
- prefer a small query-key factory inside hooks when both reads and optimistic mutations touch the same data
4. Mutation Robustness
For critical actions like "Mark as Read", we ensure higher reliability by:
- Canceling In-Flight Queries: Calling
cancelQueriesinonMutateto prevent background refetches from overwriting optimistic data. - Guaranteed Invalidations: Using
onSettledwith a fallbackuserIdto ensure related queries are always invalidated, regardless of mutation success or failure. - Conflict Resilience: Using
withRetryin the service layer to handle transient network issues during mutations.
5. Debounced Validation
Used for real-time slug checks in useSlugValidation.ts. We debounce the slug input into a debouncedSlug state, which then triggers the query. This prevents excessive API calls while typing.
Global Configuration
The QueryClient is configured in src/main.tsx with sensible defaults:
staleTime: 5 minutes (for static library data).gcTime: 10 minutes (to clean up memory).retry: 1 (to be resilient but not aggressive).refetchOnWindowFocus: false (overridden totruespecifically for analytics hooks).
Security
On sign-out (AuthContext.tsx), we call queryClient.clear() to ensure no sensitive user data (profiles, private notes, analytics) remains in the browser memory for the next session.
