# 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 return } // 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 } // 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 } ``` ## 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 ( ) } ``` ## 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 (
{commentsLoading ? : }
) } ``` ## 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