Capítulo 38 de 57
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.
loader.loader: avoids "flash of loading" states, avoids waterfall fetching from component-based fetching, and improves SEO since data is available at render time.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.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.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).new QueryClient()) must be created inside the createRouter function, not at module scope, so each request/render gets its own instance on the server.// 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>
},
})
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>
),
})
}
ensureQueryData/prefetch-style calls in loader, not returning fetched data directly, when an external cache owns the data.createRouter, never at module scope, to avoid cross-request leakage on the server.Await).dehydrate/hydrate options plug into.