Capítulo 18 de 24

Chapter 18: TanStack Query Loader Integration

Core Idea

This example shows the canonical pattern for combining TanStack Router with TanStack Query: route loaders call queryClient.ensureQueryData(queryOptions) to prefetch data before the route renders, and components then read that same cached data with useSuspenseQuery. The router's context carries the queryClient instance down to every loader.

Setup

  • Routing style: code-based (routes built with createRoute/createRootRouteWithContext and assembled via addChildren)
  • Key dependencies: @tanstack/react-router, @tanstack/react-query, @tanstack/react-query-devtools, redaxios
  • Structure: src/main.tsx defines the root route (typed with a queryClient context), the /posts layout route and $postId route, wires up createRouter with that context, and renders QueryClientProvider around RouterProvider. src/posts.ts holds the queryOptions() factories and fetch functions. src/posts.lazy.tsx is the code-split component for the posts layout, loaded via .lazy().

Code Example

// src/posts.ts
export const postQueryOptions = (postId: string) =>
  queryOptions({
    queryKey: ['posts', { postId }],
    queryFn: () => fetchPost(postId),
  })

export const postsQueryOptions = queryOptions({
  queryKey: ['posts'],
  queryFn: () => fetchPosts(),
})

// src/main.tsx
const postRoute = createRoute({
  getParentRoute: () => postsLayoutRoute,
  path: '$postId',
  errorComponent: PostErrorComponent,
  loader: ({ context: { queryClient }, params: { postId } }) =>
    queryClient.ensureQueryData(postQueryOptions(postId)),
  component: PostRouteComponent,
})

function PostRouteComponent() {
  const { postId } = postRoute.useParams()
  const postQuery = useSuspenseQuery(postQueryOptions(postId))
  const post = postQuery.data
  // ...
}

const router = createRouter({
  routeTree,
  defaultPreload: 'intent',
  // Since we're using React Query, we don't want loader calls to ever be stale
  defaultPreloadStaleTime: 0,
  context: { queryClient },
})
  • What it demonstrates: sharing one queryOptions() definition between the route loader (prefetch via ensureQueryData) and the component (read via useSuspenseQuery), so there is a single source of truth for the query key and fetcher.

Key Takeaways

  1. Set defaultPreloadStaleTime: 0 on the router when React Query owns caching, otherwise the router's own preload staleness check can skip a loader call that React Query would otherwise consider stale.
  2. Type the root route with createRootRouteWithContext<{ queryClient: QueryClient }>() so every nested loader gets a typed context.queryClient without prop drilling.
  3. Use router.invalidate() from an error boundary (paired with useQueryErrorResetBoundary) to retry a failed loader/query together.
  4. queryOptions() factories that take params (like postQueryOptions(postId)) keep query keys colocated with their fetchers, avoiding key mismatches between loader and component.

Connects To

  • kitchen-sink-react-query-file-based (ch006): a more complete file-based version of the same loader-plus-suspense-query pattern, with more routes and devtools wiring.
  • with-trpc-react-query (ch019): takes this same ensureQueryData + useQuery pattern but replaces raw queryOptions() calls with tRPC's trpc.<procedure>.queryOptions(), generated from a typed server router.