mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-03 19:37:52 +00:00
- 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
188 lines
4.6 KiB
Markdown
188 lines
4.6 KiB
Markdown
# 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
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```tsx
|
|
// 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
|