Skip to content

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

  1. Pure Services: Service files (src/services/) should only contain pure data-fetching functions. They should not manage local state or caches.
  2. Custom Hooks: All data-fetching logic is encapsulated in custom hooks within src/hooks/ (e.g., useDecks, useProfile, useLibrary).
  3. Centralized Keys: Query keys are managed at the hook level to ensure consistency during invalidation.
  4. 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.prefetchQuery for 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.

typescript
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.

typescript
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.

  1. Cancel outgoing queries for that key.
  2. Snapshot the current data.
  3. Manually update the cache.
  4. 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:

  1. Canceling In-Flight Queries: Calling cancelQueries in onMutate to prevent background refetches from overwriting optimistic data.
  2. Guaranteed Invalidations: Using onSettled with a fallback userId to ensure related queries are always invalidated, regardless of mutation success or failure.
  3. Conflict Resilience: Using withRetry in 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 to true specifically 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.

Built with ❤️ for Founders