Skip to main content

compositions-router-query

Integrating TanStack Router with TanStack Query: queryClient in router context, ensureQueryData/prefetchQuery in loaders, useSuspenseQuery in components, defaultPreloadStaleTime: 0, setupRouterSsrQueryIntegration for SSR dehydration/hydration and streaming, per-request QueryClient isolation.

الانتقال إلى التثبيت

معلومات المصدر

المستودع
hashintel/brunch
آخر نشاط في المصدر
١٦ أبريل ٢٠٢٦ في ١٠:٣٤
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٧
التفرعات
٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
compositions-router-query
description
Integrating TanStack Router with TanStack Query: queryClient in router context, ensureQueryData/prefetchQuery in loaders, useSuspenseQuery in components, defaultPreloadStaleTime: 0, setupRouterSsrQueryIntegration for SSR dehydration/hydration and streaming, per-request QueryClient isolation.
type
composition
library
tanstack-router
library_version
1.166.2
requires
["router-core","router-core-data-loading","react-router"]
sources
["TanStack/router:docs/router/guide/external-data-loading.md","TanStack/router:docs/router/integrations/query.md"]
# TanStack Router + TanStack Query Integration This skill requires familiarity with both TanStack Router and TanStack Query. Read [router-core](../router-core/SKILL.md) and [react-router](../react-router/SKILL.md) first. This skill covers coordinating TanStack Query as an external data cache with TanStack Router's loader system. The router acts as a **coordinator** — it triggers data fetching during navigation, while Query manages caching, background refetching, and data lifecycle. > **CRITICAL**: Set `defaultPreloadStaleTime: 0` when using TanStack Query. Without this, Router's built-in preload cache (30s default) prevents Query from controlling data freshness. > **CRITICAL**: For SSR, create `QueryClient` inside the `createRouter` factory function. A module-level singleton leaks data between server requests. ## Setup: QueryClient in Router Context ### Basic (Client-Only) ```tsx // src/main.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { RouterProvider, createRouter, createRootRouteWithContext, } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' // Root route declares that router context includes queryClient // (root route file creates it with createRootRouteWithContext — see below) const queryClient = new QueryClient() const router = createRouter({ routeTree, defaultPreloadStaleTime: 0, // Let Query manage caching context: { queryClient }, Wrap: ({ children }) => ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ), }) declare module '@tanstack/react-router' { interface Register { router: typeof router } } function App() { return <RouterProvider router={router} /> } ``` ### Root Route with Context ```tsx // src/routes/__root.tsx import { createRootRouteWithContext, Outlet } from '@tanstack/react-router' import type { QueryClient } from '@tanstack/react-query' // Double parentheses: factory pattern export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({ component: () => <Outlet />, }) ``` ### SSR-Safe Setup ```tsx // src/router.tsx import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createRouter } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' export function createAppRouter() { // Fresh QueryClient per request — prevents data leaking between SSR requests const queryClient = new QueryClient() return createRouter({ routeTree, defaultPreloadStaleTime: 0, context: { queryClient }, Wrap: ({ children }) => ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ), }) } declare module '@tanstack/react-router' { interface Register { router: ReturnType<typeof createAppRouter> } } ``` ### SSR with `setupRouterSsrQueryIntegration` For automatic SSR dehydration/hydration and streaming: ```bash npm install @tanstack/react-router-ssr-query ``` ```tsx // src/router.tsx import { QueryClient } from '@tanstack/react-query' import { createRouter } from '@tanstack/react-router' import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query' import { routeTree } from './routeTree.gen' export function createAppRouter() { const queryClient = new QueryClient() const router = createRouter({ routeTree, defaultPreloadStaleTime: 0, context: { queryClient }, }) setupRouterSsrQueryIntegration({ router, queryClient, // wrapQueryClient: true (default — wraps with QueryClientProvider) // handleRedirects: true (default — handles redirect() from queries) }) return router } ``` The integration: - Dehydrates query state on the server and hydrates on the client automatically - Streams queries that resolve during server render to the client - Handles `redirect()` thrown from queries/mutations ### Manual SSR Dehydration/Hydration (Without SSR Query Package) ```tsx // src/router.tsx import { QueryClient, dehydrate, hydrate } from '@tanstack/react-query' import { QueryClientProvider } from '@tanstack/react-query' import { createRouter } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' export function createAppRouter() { const queryClient = new QueryClient() return createRouter({ routeTree, defaultPreloadStaleTime: 0, context: { queryClient }, dehydrate: () => ({ queryClientState: dehydrate(queryClient), }), hydrate: (dehydrated) => { hydrate(queryClient, dehydrated.queryClientState) }, Wrap: ({ children }) => ( <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> ), }) } ``` ## Core Pattern: `ensureQueryData` in Loader + `useSuspenseQuery` in Component This is the recommended pattern. The loader ensures data is in the cache before render (no loading flash). The component subscribes to the cache for updates. ```tsx // src/routes/posts.tsx import { queryOptions, useSuspenseQuery } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' interface Post { id: string title: string } const postsQueryOptions = queryOptions({ queryKey: ['posts'], queryFn: (): Promise<Array<Post>> => fetch('/api/posts').then((r) => r.json()), }) export const Route = createFileRoute('/posts')({ loader: ({ context }) => { // ensureQueryData returns cached data if available, fetches if not in cache // To also refetch stale data, pass revalidateIfStale: true return context.queryClient.ensureQueryData(postsQueryOptions) }, component: PostsPage, }) function PostsPage() { // useSuspenseQuery subscribes to cache — gets background updates const { data: posts } = useSuspenseQuery(postsQueryOptions) return ( <ul> {posts.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ) } ``` ### With Dynamic Params ```tsx // src/routes/posts/$postId.tsx import { queryOptions, useSuspenseQuery } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' interface Post { id: string title: string content: string } const postQueryOptions = (postId: string) => queryOptions({ queryKey: ['posts', postId], queryFn: () => fetch(`/api/posts/${postId}`).then((r) => r.json()), }) export const Route = createFileRoute('/posts/$postId')({ loader: ({ context, params }) => { return context.queryClient.ensureQueryData(postQueryOptions(params.postId)) }, component: PostPage, }) function PostPage() { const { postId } = Route.useParams() const { data: post } = useSuspenseQuery(postQueryOptions(postId)) return <article>{post.title}</article> } ``` ## Streaming Pattern: `prefetchQuery` (Not Awaited) For non-critical data, start the fetch without blocking navigation: ```tsx import { useQuery, useSuspenseQuery } from '@tanstack/react-query' export const Route = createFileRoute('/dashboard')({ loader: ({ context }) => { // Await critical data const user = context.queryClient.ensureQueryData(userQueryOptions) // Start non-critical fetch without awaiting — streams during SSR context.queryClient.prefetchQuery(analyticsQueryOptions) return user }, component: Dashboard, }) function Dashboard() { // Critical: suspense (data ready immediately) const { data: user } = useSuspenseQuery(userQueryOptions) // Non-critical: regular query (shows loading state) const { data: analytics, isLoading } = useQuery(analyticsQueryOptions) return ( <div> <h1>Welcome {user.name}</h1> {isLoading ? <Skeleton /> : <AnalyticsChart data={analytics} />} </div> ) } ``` ## Error Handling with `useQueryErrorResetBoundary` ```tsx import { useEffect } from 'react' import { useQueryErrorResetBoundary } from '@tanstack/react-query' import { useRouter } from '@tanstack/react-router' export const Route = createFileRoute('/posts')({ loader: ({ context }) => context.queryClient.ensureQueryData(postsQueryOptions), errorComponent: PostsErrorComponent, component: PostsPage, }) function PostsErrorComponent({ error, reset, }: { error: Error reset: () => void }) { const router = useRouter() const queryErrorResetBoundary = useQueryErrorResetBoundary() useEffect(() => { queryErrorResetBoundary.reset() }, [queryErrorResetBoundary]) return ( <div> <p>{error.message}</p> <button onClick={() => router.invalidate()}>Retry</button> </div> ) } ``` ## Common Mistakes ### 1. HIGH: Not setting `defaultPreloadStaleTime` to 0 Router has a built-in preload cache (default `staleTime` for preloads is 30s). This prevents Query from controlling data freshness during preloading. ```tsx // WRONG — Router's preload cache serves stale data, Query never refetches const router = createRouter({ routeTree }) // CORRECT — disable Router's preload cache, let Query manage freshness const router = createRouter({ routeTree, defaultPreloadStaleTime: 0, }) ``` ### 2. HIGH: Creating QueryClient outside `createRouter` for SSR A module-level singleton `QueryClient` is shared across all server requests, leaking user data between requests. ```tsx // WRONG — shared across SSR requests const queryClient = new QueryClient() export function createAppRouter() { return createRouter({ routeTree, context: { queryClient }, }) } // CORRECT — new QueryClient per createAppRouter call export function createAppRouter() { const queryClient = new QueryClient() return createRouter({ routeTree, context: { queryClient }, }) } ``` ### 3. MEDIUM: Awaiting `prefetchQuery` in loader blocks rendering `prefetchQuery` is designed to fire-and-forget. Awaiting it blocks the navigation transition until the data resolves, defeating the purpose of streaming. ```tsx // WRONG — blocks navigation, no streaming benefit loader: async ({ context }) => { await context.queryClient.prefetchQuery(analyticsQueryOptions) } // CORRECT — fire and forget for streaming loader: ({ context }) => { context.queryClient.prefetchQuery(analyticsQueryOptions) } // If you need to block (critical data), use ensureQueryData instead: loader: ({ context }) => { return context.queryClient.ensureQueryData(criticalQueryOptions) } ``` ### 4. HIGH: Missing double parentheses on `createRootRouteWithContext` `createRootRouteWithContext<Type>()` is a factory — it returns a function. The second call passes route options. ```tsx // WRONG — passing options to the factory, not the returned function const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>({ component: RootComponent, }) // CORRECT — double call: factory()({options}) const rootRoute = createRootRouteWithContext<{ queryClient: QueryClient }>()({ component: RootComponent, }) ``` ## Tension: Built-In SWR Cache vs External Cache TanStack Router has its own SWR cache (`staleTime`, `gcTime`, `defaultPreloadStaleTime`). When using Query as an external cache: - Set `defaultPreloadStaleTime: 0` to prevent Router's cache from short-circuiting Query's freshness logic - Router's `staleTime`/`gcTime` still apply to the loader return value. For pure Query patterns, return nothing from the loader (just `ensureQueryData` for the side effect) and read data exclusively from `useSuspenseQuery` - `router.invalidate()` re-runs loaders (which call `ensureQueryData`), but Query decides whether to actually refetch based on its own `staleTime` ## Cross-References - [router-core-data-loading](../router-core-data-loading/SKILL.md) — built-in loader caching fundamentals - [router-core-ssr](../router-core-ssr/SKILL.md) — SSR setup for dehydration/hydration
عرض على GitHub