Capítulo 38 de 57

Chapter 38: External Data Loading

Core Idea

TanStack Router is designed to act as a coordinator, not a mandatory store, for data fetching: any promise-based library (TanStack Query, SWR, RTK Query, urql, Relay, Apollo, or even state stores like Zustand/Jotai/Redux) can be wired into route loaders so the router still preloads and sequences data in line with navigation.

Key Concepts

  • Store vs. coordinate: the router can either store data itself (built-in cache, see Ch 36) or simply coordinate an external library's fetching/caching by calling into it from loader.
  • Why preload in loader: avoids "flash of loading" states, avoids waterfall fetching from component-based fetching, and improves SEO since data is available at render time.
  • Seeding an external cache: the loader calls the external library's prefetch/ensure API (e.g. queryClient.ensureQueryData(postsQueryOptions)), and the component reads from that same cache with the library's hook (e.g. useSuspenseQuery(postsQueryOptions)), rather than returning data from the loader directly.
  • queryOptions: TanStack Query pattern for defining a reusable { queryKey, queryFn } object shared between the loader's prefetch call and the component's useSuspenseQuery call.
  • Error handling with TanStack Query: use useQueryErrorResetBoundary's reset() in a useEffect inside errorComponent so the query retries correctly, including when the user navigates away and back, combined with router.invalidate() on a manual retry button.
  • Critical dehydration/hydration: createRouter({ dehydrate, hydrate, Wrap }) options let you serialize an external cache's state on the server and rehydrate it on the client; dehydrate returns serializable JSON merged into the router's payload, hydrate restores it, and Wrap wraps the app in the necessary provider (e.g. QueryClientProvider).
  • Per-request store creation: the external cache/store (e.g. new QueryClient()) must be created inside the createRouter function, not at module scope, so each request/render gets its own instance on the server.

Code Examples

// src/routes/posts.tsx
const postsQueryOptions = queryOptions({
  queryKey: ['posts'],
  queryFn: () => fetchPosts(),
})

export const Route = createFileRoute('/posts')({
  loader: () => queryClient.ensureQueryData(postsQueryOptions),
  component: () => {
    const { data: { posts } } = useSuspenseQuery(postsQueryOptions)
    return <div>{posts.map((post) => <Post key={post.id} post={post} />)}</div>
  },
})
  • What it demonstrates: using the loader only to guarantee data is in the TanStack Query cache before render, while the component reads via useSuspenseQuery.
// src/router.tsx
export function createRouter() {
  const queryClient = new QueryClient()
  return createRouter({
    routeTree,
    context: { queryClient },
    dehydrate: () => ({ queryClientState: dehydrate(queryClient) }),
    hydrate: (dehydrated) => hydrate(queryClient, dehydrated.queryClientState),
    Wrap: ({ children }) => (
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    ),
  })
}
  • What it demonstrates: wiring TanStack Query's dehydrate/hydrate lifecycle into the router's SSR dehydration/hydration hooks.

Key Takeaways

  1. Prefer ensureQueryData/prefetch-style calls in loader, not returning fetched data directly, when an external cache owns the data.
  2. Always create per-request state (query clients, stores) inside createRouter, never at module scope, to avoid cross-request leakage on the server.
  3. Any promise-returning library works; the router doesn't care which one you pick.

Connects To

  • Ch 36: Data Loading's built-in cache is the alternative to this pattern; this chapter is for when you need more robust caching, mutation APIs, or persistence.
  • Ch 37: Deferred Data Loading covers the external-library variant of deferring slow data (prefetch + Suspense instead of Await).
  • Ch 42: SSR, the general dehydration/hydration and streaming mechanics that this chapter's dehydrate/hydrate options plug into.