Files
dotfiles/dot_agents/skills/tanstack-integration-best-practices/rules/flow-loader-query-pattern.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

179 lines
4.8 KiB
Markdown

# flow-loader-query-pattern: Use Loaders with ensureQueryData
## Priority: HIGH
## Explanation
The recommended pattern combines TanStack Router's loaders with TanStack Query's `ensureQueryData`. Loaders prefetch data during navigation, while Query manages caching and updates. Use `useSuspenseQuery` in components since data is guaranteed.
## Bad Example
```tsx
// Only using Query in component - loading waterfall
function PostsPage() {
const { data, isLoading } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
})
if (isLoading) return <Loading />
return <PostList posts={data} />
}
// Only using loader - no cache management
export const Route = createFileRoute('/posts')({
loader: async () => {
const posts = await fetchPosts() // Not cached
return { posts }
},
})
```
## Good Example
```tsx
// lib/queries/posts.ts - Define queryOptions
import { queryOptions } from '@tanstack/react-query'
export const postQueries = {
all: () => queryOptions({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 5 * 60 * 1000,
}),
detail: (id: string) => queryOptions({
queryKey: ['posts', id],
queryFn: () => fetchPost(id),
staleTime: 5 * 60 * 1000,
}),
}
// routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
import { useSuspenseQuery } from '@tanstack/react-query'
import { postQueries } from '@/lib/queries/posts'
export const Route = createFileRoute('/posts')({
loader: async ({ context: { queryClient } }) => {
// Prefetch in loader - runs during navigation
await queryClient.ensureQueryData(postQueries.all())
},
component: PostsPage,
})
function PostsPage() {
// Data guaranteed by loader - no loading state needed
const { data: posts } = useSuspenseQuery(postQueries.all())
return <PostList posts={posts} />
}
// routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params, context: { queryClient } }) => {
await queryClient.ensureQueryData(postQueries.detail(params.postId))
},
component: PostDetailPage,
})
function PostDetailPage() {
const { postId } = Route.useParams()
const { data: post } = useSuspenseQuery(postQueries.detail(postId))
return <PostContent post={post} />
}
```
## Good Example: Parallel Data Loading
```tsx
export const Route = createFileRoute('/dashboard')({
loader: async ({ context: { queryClient } }) => {
// Load multiple queries in parallel
await Promise.all([
queryClient.ensureQueryData(statsQueries.overview()),
queryClient.ensureQueryData(activityQueries.recent()),
queryClient.ensureQueryData(userQueries.current()),
])
},
component: DashboardPage,
})
function DashboardPage() {
// All data ready - no loading states
const { data: stats } = useSuspenseQuery(statsQueries.overview())
const { data: activity } = useSuspenseQuery(activityQueries.recent())
const { data: user } = useSuspenseQuery(userQueries.current())
return (
<Dashboard
stats={stats}
activity={activity}
user={user}
/>
)
}
```
## Good Example: Optional Prefetch with Non-Critical Data
```tsx
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params, context: { queryClient } }) => {
// Critical data - await it
await queryClient.ensureQueryData(postQueries.detail(params.postId))
// Non-critical - prefetch but don't await
queryClient.prefetchQuery(postQueries.comments(params.postId))
queryClient.prefetchQuery(postQueries.related(params.postId))
},
component: PostPage,
})
function PostPage() {
const { postId } = Route.useParams()
// Critical - guaranteed by loader
const { data: post } = useSuspenseQuery(postQueries.detail(postId))
// Non-critical - may still be loading
const { data: comments, isLoading: commentsLoading } = useQuery(
postQueries.comments(postId)
)
return (
<article>
<PostContent post={post} />
{commentsLoading ? <CommentsSkeleton /> : <Comments data={comments} />}
</article>
)
}
```
## Data Flow Summary
```
Navigation Starts
↓
Router matches route
↓
loader() executes
↓
ensureQueryData() checks cache
↓
Fresh cache? → Return cached Stale/missing? → Fetch and cache
↓ ↓
Route renders Route renders
↓ ↓
useSuspenseQuery returns data useSuspenseQuery returns data
```
## Context
- `ensureQueryData` respects staleTime - won't refetch fresh data
- `useSuspenseQuery` throws promise to Suspense if data missing
- Loaders enable preloading on link hover
- This pattern eliminates loading waterfalls
- Use `useQuery` for non-critical data that can load after render
- Query invalidation and background updates still work normally