Files
dotfiles/dot_agents/skills/tanstack-integration-best-practices/rules/cache-single-source.md
T
Prad Nukala 6406208b35 shell: fish functions -> ~/.local/bin executables + bash/zsh ports; manage ~/.agents
- 16 cross-shell utilities converted to sh/bash chezmoi executables:
  ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone
  ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj
  (ghg-clone not named ghg: unrelated cargo binary owns that name)
- cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho
  cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files
- fish keeps only shell-state functions; converted ones now hit PATH
- cp-appid ignore gating moved to .local/bin path
- dot_agents/skills: 82 skills materialized from dotstate storage so
  dot_claude skill symlinks resolve on fresh bootstrap; dotstate
  profile/symlinks.json released .agents/skills .bashrc .zshrc
2026-08-10 15:15:06 -04:00

4.6 KiB

cache-single-source: Let TanStack Query Manage Caching

Priority: MEDIUM

Explanation

When using TanStack Router with TanStack Query, let Query be the single source of truth for caching. Disable Router's built-in cache with defaultPreloadStaleTime: 0 to avoid confusion about which cache is authoritative.

Bad Example

// Both Router and Query caching enabled - confusing
const router = createRouter({
  routeTree,
  context: { queryClient },
  // Default router caching enabled
  // defaultPreloadStaleTime: 30000 (default)
})

export const Route = createFileRoute('/posts')({
  loader: async () => {
    // Fetches directly - cached by Router
    const posts = await fetchPosts()
    return { posts }
  },
  component: PostsPage,
})

function PostsPage() {
  // Also uses Query cache - which is authoritative?
  const { data } = useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
  })

  // Now there are TWO caches with potentially different data
}

Good Example

// router.tsx - Disable router cache when using Query
import { QueryClient } from '@tanstack/react-query'
import { createRouter } from '@tanstack/react-router'
import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
import { routeTree } from './routeTree.gen'

export function getRouter() {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 1000 * 60 * 2, // 2 minutes
        refetchOnWindowFocus: false,
      },
    },
  })

  const router = createRouter({
    routeTree,
    context: { queryClient },
    defaultPreload: 'intent',
    defaultPreloadStaleTime: 0, // Let Query manage caching
    scrollRestoration: true,
  })

  setupRouterSsrQueryIntegration({
    router,
    queryClient,
  })

  return router
}

// routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: async ({ context: { queryClient } }) => {
    // Query is the single cache source
    await queryClient.ensureQueryData(postQueries.all())
    // No return needed - data lives in Query cache
  },
  component: PostsPage,
})

function PostsPage() {
  // Single source of truth
  const { data: posts } = useSuspenseQuery(postQueries.all())

  return <PostList posts={posts} />
}

Cache Comparison

Feature Router Cache Query Cache
Invalidation Manual/time-based Query keys, patterns
Background refetch No Yes
Optimistic updates No Yes
Mutations No built-in Full support
DevTools Limited Rich debugging
Cross-route sharing Full Full

Good Example: Coordinated Caching Config

// router.tsx
export function getRouter() {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000,       // Fresh for 1 minute
        gcTime: 10 * 60 * 1000,     // Cache for 10 minutes
        refetchOnWindowFocus: true, // Refetch when tab focused
        retry: 1,
      },
    },
  })

  const router = createRouter({
    routeTree,
    context: { queryClient },
    defaultPreload: 'intent',
    defaultPreloadStaleTime: 0, // Router defers to Query
    scrollRestoration: true,
    defaultStructuralSharing: true,
  })

  setupRouterSsrQueryIntegration({
    router,
    queryClient,
  })

  return router
}

Good Example: Preload Still Works

// Preloading still works - it just uses Query's cache
export function getRouter() {
  const queryClient = new QueryClient()

  const router = createRouter({
    routeTree,
    context: { queryClient },
    defaultPreload: 'intent',     // Preload on hover
    defaultPreloadStaleTime: 0,   // Query decides if data is stale
  })

  setupRouterSsrQueryIntegration({ router, queryClient })

  return router
}

// When user hovers a Link:
// 1. Router triggers preload
// 2. Loader runs ensureQueryData
// 3. Query checks its cache - fresh? skip fetch. stale? refetch.
// 4. User clicks - data already in Query cache

Mutation Invalidation

// Mutations properly invalidate the single cache
const createPost = useMutation({
  mutationFn: submitPost,
  onSuccess: () => {
    // Invalidate Query cache - the single source
    queryClient.invalidateQueries({ queryKey: ['posts'] })

    // Router automatically uses updated cache on next navigation
    navigate({ to: '/posts' })
  },
})

Context

  • defaultPreloadStaleTime: 0 means "always ask Query"
  • Query's staleTime/gcTime controls caching behavior
  • Preloading still works - just uses Query's cache
  • Mutations, optimistic updates, invalidation all work normally
  • DevTools show the single authoritative cache state
  • Use setupRouterSsrQueryIntegration for SSR hydration