Files
findyourpilot/.agents/skills/tanstack-router-best-practices/rules/load-ensure-query-data.md
2026-03-02 21:16:26 +01:00

4.1 KiB

load-ensure-query-data: Use ensureQueryData with TanStack Query

Priority: HIGH

Explanation

When integrating TanStack Router with TanStack Query, use queryClient.ensureQueryData() in loaders instead of prefetchQuery(). This respects the cache, awaits data if missing, and returns the data for potential use.

Bad Example

// Using prefetchQuery - doesn't return data, can't await stale check
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params, context: { queryClient } }) => {
    // prefetchQuery never throws, swallows errors
    queryClient.prefetchQuery({
      queryKey: ['posts', params.postId],
      queryFn: () => fetchPost(params.postId),
    })
    // No await - might not complete before render
    // No return value to use
  },
})

// Fetching directly - bypasses TanStack Query cache
export const Route = createFileRoute('/posts')({
  loader: async () => {
    const posts = await fetchPosts()  // Not cached
    return { posts }
  },
})

Good Example

// Define queryOptions for reuse
const postQueryOptions = (postId: string) =>
  queryOptions({
    queryKey: ['posts', postId],
    queryFn: () => fetchPost(postId),
    staleTime: 5 * 60 * 1000,  // 5 minutes
  })

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params, context: { queryClient } }) => {
    // ensureQueryData:
    // - Returns cached data if fresh
    // - Fetches and caches if missing or stale
    // - Awaits completion
    // - Throws on error (caught by error boundary)
    await queryClient.ensureQueryData(postQueryOptions(params.postId))
  },
  component: PostPage,
})

function PostPage() {
  const { postId } = Route.useParams()

  // Data guaranteed to exist from loader
  const { data: post } = useSuspenseQuery(postQueryOptions(postId))

  return <PostContent post={post} />
}

Good Example: Multiple Parallel Queries

export const Route = createFileRoute('/dashboard')({
  loader: async ({ context: { queryClient } }) => {
    // Parallel data fetching
    await Promise.all([
      queryClient.ensureQueryData(statsQueries.overview()),
      queryClient.ensureQueryData(activityQueries.recent()),
      queryClient.ensureQueryData(notificationQueries.unread()),
    ])
  },
})

Good Example: Dependent Queries

export const Route = createFileRoute('/users/$userId/posts')({
  loader: async ({ params, context: { queryClient } }) => {
    // First query needed for second
    const user = await queryClient.ensureQueryData(
      userQueries.detail(params.userId)
    )

    // Dependent query uses result
    await queryClient.ensureQueryData(
      postQueries.byAuthor(user.id)
    )
  },
})

Router Configuration for TanStack Query

// router.tsx
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 60 * 1000,  // 1 minute default
    },
  },
})

export const router = createRouter({
  routeTree,
  context: { queryClient },

  // Let TanStack Query manage caching
  defaultPreloadStaleTime: 0,

  // SSR: Dehydrate query cache
  dehydrate: () => ({
    queryClientState: dehydrate(queryClient),
  }),

  // SSR: Hydrate on client
  hydrate: (dehydrated) => {
    hydrate(queryClient, dehydrated.queryClientState)
  },

  // Wrap with QueryClientProvider
  Wrap: ({ children }) => (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  ),
})

ensureQueryData vs prefetchQuery vs fetchQuery

Method Returns Throws Awaits Use Case
ensureQueryData Data Yes Yes Route loaders (recommended)
prefetchQuery void No Yes Background prefetching
fetchQuery Data Yes Yes When you need data immediately

Context

  • ensureQueryData is the recommended method for route loaders
  • Respects staleTime - won't refetch fresh cached data
  • Errors propagate to route error boundaries
  • Use queryOptions() factory for type-safe, reusable query definitions
  • Set defaultPreloadStaleTime: 0 to let TanStack Query manage cache
  • Pair with useSuspenseQuery in components for guaranteed data