
Tanstack Integration Best Practices
- 2.3k installs
- 205 repo stars
- Updated April 3, 2026
- deckardger/tanstack-agent-skills
tanstack-integration-best-practices is an agent skill that Best practices for integrating TanStack Query with TanStack Router and TanStack Start. Patterns for full-stack data flow.
About
Guidelines for integrating TanStack Query Router and Start together effectively These patterns ensure optimal data flow caching coordination and type safety across the stack Setting up a new TanStack Start project Integrating TanStack Query with TanStack Router Configuring SSR with query hydration Coordinating caching between router and query Setting up type safe data fetching patterns Priority Category Rules Impact CRITICAL Setup 3 rules Foundational configuration CRITICAL SSR Integration 1 rule Router Query SSR setup HIGH Data Flow 4 rules Correct data fetching patterns MEDIUM Caching 3 rules Performance optimization MEDIUM SSR 2 rules Additional SSR patterns setup query client context Pass QueryClient through router context setup provider wrapping Correctly wrap with QueryClientProvider setup stale time coordination Coordinate staleTime between router and query flow loader query pattern Use loaders with ensureQueryData flow suspense query component Use useSuspenseQuery in components flow mutations invalidation Coordinate mutations with query invalidation flow server functions queries Use server functions for query functions
- name: tanstack-integration-best-practices
- description: Best practices for integrating TanStack Query with TanStack Router and TanStack Start. Patterns for full-st
- Guidelines for integrating TanStack Query, Router, and Start together effectively. These patterns ensure optimal data fl
- Follow tanstack-integration-best-practices SKILL.md steps and documented constraints.
- Follow tanstack-integration-best-practices SKILL.md steps and documented constraints.
Tanstack Integration Best Practices by the numbers
- 2,265 all-time installs (skills.sh)
- +49 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #472 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
tanstack-integration-best-practices capabilities & compatibility
- Capabilities
- name: tanstack integration best practices · description: best practices for integrating tans · guidelines for integrating tanstack query, route · follow tanstack integration best practices skill
- Use cases
- orchestration
What tanstack-integration-best-practices says it does
name: tanstack-integration-best-practices
description: Best practices for integrating TanStack Query with TanStack Router and TanStack Start. Patterns for full-stack data flow, SSR, and caching coordination.
Guidelines for integrating TanStack Query, Router, and Start together effectively. These patterns ensure optimal data flow, caching coordination, and type safety across the stack.
npx skills add https://github.com/deckardger/tanstack-agent-skills --skill tanstack-integration-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.3k |
|---|---|
| repo stars | ★ 205 |
| Security audit | 3 / 3 scanners passed |
| Last updated | April 3, 2026 |
| Repository | deckardger/tanstack-agent-skills ↗ |
When should an agent use tanstack-integration-best-practices and what problem does it solve?
Best practices for integrating TanStack Query with TanStack Router and TanStack Start. Patterns for full-stack data flow, SSR, and caching coordination.
Who is it for?
Developers invoking tanstack-integration-best-practices as documented in the skill source.
Skip if: Skip when requirements fall outside tanstack-integration-best-practices documented scope.
When should I use this skill?
Best practices for integrating TanStack Query with TanStack Router and TanStack Start. Patterns for full-stack data flow, SSR, and caching coordination.
What you get
Outputs aligned with the tanstack-integration-best-practices SKILL.md workflow and stated deliverables.
- TanStack Router config with Query as single cache authority
By the numbers
- Rated MEDIUM priority in tanstack-agent-skills
- Documents Router defaultPreloadStaleTime of 30000 ms as the conflicting default
Files
TanStack Integration Best Practices
Guidelines for integrating TanStack Query, Router, and Start together effectively. These patterns ensure optimal data flow, caching coordination, and type safety across the stack.
When to Apply
- Setting up a new TanStack Start project
- Integrating TanStack Query with TanStack Router
- Configuring SSR with query hydration
- Coordinating caching between router and query
- Setting up type-safe data fetching patterns
Rule Categories by Priority
| Priority | Category | Rules | Impact |
|---|---|---|---|
| CRITICAL | Setup | 3 rules | Foundational configuration |
| CRITICAL | SSR Integration | 1 rule | Router + Query SSR setup |
| HIGH | Data Flow | 4 rules | Correct data fetching patterns |
| MEDIUM | Caching | 3 rules | Performance optimization |
| MEDIUM | SSR | 2 rules | Additional SSR patterns |
Quick Reference
Setup (Prefix: setup-)
setup-query-client-context— Pass QueryClient through router contextsetup-provider-wrapping— Correctly wrap with QueryClientProvidersetup-stale-time-coordination— Coordinate staleTime between router and query
Data Flow (Prefix: flow-)
flow-loader-query-pattern— Use loaders with ensureQueryDataflow-suspense-query-component— Use useSuspenseQuery in componentsflow-mutations-invalidation— Coordinate mutations with query invalidationflow-server-functions-queries— Use server functions for query functions
Caching (Prefix: cache-)
cache-single-source— Let TanStack Query manage cachingcache-preload-coordination— Coordinate preloading between router and querycache-invalidation-patterns— Unified invalidation patterns
SSR Integration (Prefix: ssr-)
ssr-dehydrate-hydrate— Use setupRouterSsrQueryIntegration for automatic SSR
Additional SSR (Prefix: ssr-)
ssr-per-request-client— Create QueryClient per requestssr-streaming-queries— Handle streaming with queries
How to Use
Each rule file in the rules/ directory contains: 1. Explanation — Why this pattern matters 2. Bad Example — Anti-pattern to avoid 3. Good Example — Recommended implementation 4. Context — When to apply or skip this rule
Full Reference
See individual rule files in rules/ directory for detailed guidance and code examples.
cache-single-source: Let TanStack Query Manage Caching
Priority: MEDIUM
Explanation
When using TanStack Router with TanStack Query, let Query be the single source of truth for caching. Disable Router's built-in cache with defaultPreloadStaleTime: 0 to avoid confusion about which cache is authoritative.
Bad Example
// Both Router and Query caching enabled - confusing
const router = createRouter({
routeTree,
context: { queryClient },
// Default router caching enabled
// defaultPreloadStaleTime: 30000 (default)
})
export const Route = createFileRoute('/posts')({
loader: async () => {
// Fetches directly - cached by Router
const posts = await fetchPosts()
return { posts }
},
component: PostsPage,
})
function PostsPage() {
// Also uses Query cache - which is authoritative?
const { data } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
})
// Now there are TWO caches with potentially different data
}Good Example
// router.tsx - Disable router cache when using Query
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 getRouter() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 2, // 2 minutes
refetchOnWindowFocus: false,
},
},
})
const router = createRouter({
routeTree,
context: { queryClient },
defaultPreload: 'intent',
defaultPreloadStaleTime: 0, // Let Query manage caching
scrollRestoration: true,
})
setupRouterSsrQueryIntegration({
router,
queryClient,
})
return router
}
// routes/posts.tsx
export const Route = createFileRoute('/posts')({
loader: async ({ context: { queryClient } }) => {
// Query is the single cache source
await queryClient.ensureQueryData(postQueries.all())
// No return needed - data lives in Query cache
},
component: PostsPage,
})
function PostsPage() {
// Single source of truth
const { data: posts } = useSuspenseQuery(postQueries.all())
return <PostList posts={posts} />
}Cache Comparison
| Feature | Router Cache | Query Cache |
|---|---|---|
| Invalidation | Manual/time-based | Query keys, patterns |
| Background refetch | No | Yes |
| Optimistic updates | No | Yes |
| Mutations | No built-in | Full support |
| DevTools | Limited | Rich debugging |
| Cross-route sharing | Full | Full |
Good Example: Coordinated Caching Config
// router.tsx
export function getRouter() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // Fresh for 1 minute
gcTime: 10 * 60 * 1000, // Cache for 10 minutes
refetchOnWindowFocus: true, // Refetch when tab focused
retry: 1,
},
},
})
const router = createRouter({
routeTree,
context: { queryClient },
defaultPreload: 'intent',
defaultPreloadStaleTime: 0, // Router defers to Query
scrollRestoration: true,
defaultStructuralSharing: true,
})
setupRouterSsrQueryIntegration({
router,
queryClient,
})
return router
}Good Example: Preload Still Works
// Preloading still works - it just uses Query's cache
export function getRouter() {
const queryClient = new QueryClient()
const router = createRouter({
routeTree,
context: { queryClient },
defaultPreload: 'intent', // Preload on hover
defaultPreloadStaleTime: 0, // Query decides if data is stale
})
setupRouterSsrQueryIntegration({ router, queryClient })
return router
}
// When user hovers a Link:
// 1. Router triggers preload
// 2. Loader runs ensureQueryData
// 3. Query checks its cache - fresh? skip fetch. stale? refetch.
// 4. User clicks - data already in Query cacheMutation Invalidation
// Mutations properly invalidate the single cache
const createPost = useMutation({
mutationFn: submitPost,
onSuccess: () => {
// Invalidate Query cache - the single source
queryClient.invalidateQueries({ queryKey: ['posts'] })
// Router automatically uses updated cache on next navigation
navigate({ to: '/posts' })
},
})Context
defaultPreloadStaleTime: 0means "always ask Query"- Query's staleTime/gcTime controls caching behavior
- Preloading still works - just uses Query's cache
- Mutations, optimistic updates, invalidation all work normally
- DevTools show the single authoritative cache state
- Use
setupRouterSsrQueryIntegrationfor SSR hydration
flow-loader-query-pattern: Use Loaders with ensureQueryData
Priority: HIGH
Explanation
The recommended pattern combines TanStack Router's loaders with TanStack Query's ensureQueryData. Loaders prefetch data during navigation, while Query manages caching and updates. Use useSuspenseQuery in components since data is guaranteed.
Bad Example
// Only using Query in component - loading waterfall
function PostsPage() {
const { data, isLoading } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
})
if (isLoading) return <Loading />
return <PostList posts={data} />
}
// Only using loader - no cache management
export const Route = createFileRoute('/posts')({
loader: async () => {
const posts = await fetchPosts() // Not cached
return { posts }
},
})Good Example
// lib/queries/posts.ts - Define queryOptions
import { queryOptions } from '@tanstack/react-query'
export const postQueries = {
all: () => queryOptions({
queryKey: ['posts'],
queryFn: fetchPosts,
staleTime: 5 * 60 * 1000,
}),
detail: (id: string) => queryOptions({
queryKey: ['posts', id],
queryFn: () => fetchPost(id),
staleTime: 5 * 60 * 1000,
}),
}
// routes/posts.tsx
import { createFileRoute } from '@tanstack/react-router'
import { useSuspenseQuery } from '@tanstack/react-query'
import { postQueries } from '@/lib/queries/posts'
export const Route = createFileRoute('/posts')({
loader: async ({ context: { queryClient } }) => {
// Prefetch in loader - runs during navigation
await queryClient.ensureQueryData(postQueries.all())
},
component: PostsPage,
})
function PostsPage() {
// Data guaranteed by loader - no loading state needed
const { data: posts } = useSuspenseQuery(postQueries.all())
return <PostList posts={posts} />
}
// routes/posts/$postId.tsx
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params, context: { queryClient } }) => {
await queryClient.ensureQueryData(postQueries.detail(params.postId))
},
component: PostDetailPage,
})
function PostDetailPage() {
const { postId } = Route.useParams()
const { data: post } = useSuspenseQuery(postQueries.detail(postId))
return <PostContent post={post} />
}Good Example: Parallel Data Loading
export const Route = createFileRoute('/dashboard')({
loader: async ({ context: { queryClient } }) => {
// Load multiple queries in parallel
await Promise.all([
queryClient.ensureQueryData(statsQueries.overview()),
queryClient.ensureQueryData(activityQueries.recent()),
queryClient.ensureQueryData(userQueries.current()),
])
},
component: DashboardPage,
})
function DashboardPage() {
// All data ready - no loading states
const { data: stats } = useSuspenseQuery(statsQueries.overview())
const { data: activity } = useSuspenseQuery(activityQueries.recent())
const { data: user } = useSuspenseQuery(userQueries.current())
return (
<Dashboard
stats={stats}
activity={activity}
user={user}
/>
)
}Good Example: Optional Prefetch with Non-Critical Data
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params, context: { queryClient } }) => {
// Critical data - await it
await queryClient.ensureQueryData(postQueries.detail(params.postId))
// Non-critical - prefetch but don't await
queryClient.prefetchQuery(postQueries.comments(params.postId))
queryClient.prefetchQuery(postQueries.related(params.postId))
},
component: PostPage,
})
function PostPage() {
const { postId } = Route.useParams()
// Critical - guaranteed by loader
const { data: post } = useSuspenseQuery(postQueries.detail(postId))
// Non-critical - may still be loading
const { data: comments, isLoading: commentsLoading } = useQuery(
postQueries.comments(postId)
)
return (
<article>
<PostContent post={post} />
{commentsLoading ? <CommentsSkeleton /> : <Comments data={comments} />}
</article>
)
}Data Flow Summary
Navigation Starts
↓
Router matches route
↓
loader() executes
↓
ensureQueryData() checks cache
↓
Fresh cache? → Return cached Stale/missing? → Fetch and cache
↓ ↓
Route renders Route renders
↓ ↓
useSuspenseQuery returns data useSuspenseQuery returns dataContext
ensureQueryDatarespects staleTime - won't refetch fresh datauseSuspenseQuerythrows promise to Suspense if data missing- Loaders enable preloading on link hover
- This pattern eliminates loading waterfalls
- Use
useQueryfor non-critical data that can load after render - Query invalidation and background updates still work normally
setup-query-client-context: Pass QueryClient Through Router Context
Priority: CRITICAL
Explanation
Pass the QueryClient instance through TanStack Router's context system rather than using a global. This enables proper SSR with per-request clients, testability, and type-safe access in loaders. Use @tanstack/react-router-ssr-query for automatic SSR integration.
Bad Example
// lib/query-client.ts - Global singleton
export const queryClient = new QueryClient()
// routes/posts.tsx - Importing global
import { queryClient } from '@/lib/query-client'
export const Route = createFileRoute('/posts')({
loader: async () => {
// Using global - breaks SSR, harder to test
return queryClient.fetchQuery(postQueries.list())
},
})Good Example: Modern Router Setup
// routes/__root.tsx
import { createRootRouteWithContext } from '@tanstack/react-router'
import { QueryClient } from '@tanstack/react-query'
interface RouterContext {
queryClient: QueryClient
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootComponent,
})
// 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 getRouter() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
staleTime: 1000 * 60 * 2, // 2 minutes
},
},
})
const router = createRouter({
routeTree,
context: { queryClient },
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
scrollRestoration: true,
})
setupRouterSsrQueryIntegration({
router,
queryClient,
})
return router
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof getRouter>
}
}
// routes/posts.tsx - Access from context
export const Route = createFileRoute('/posts')({
loader: async ({ context: { queryClient } }) => {
// Type-safe access to queryClient from context
await queryClient.ensureQueryData(postQueries.list())
},
})Good Example: Root Route with Context
// routes/__root.tsx
import { createRootRouteWithContext, Outlet, HeadContent, Scripts } from '@tanstack/react-router'
import { QueryClient } from '@tanstack/react-query'
interface RouterContext {
queryClient: QueryClient
user: User | null
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: RootComponent,
beforeLoad: async ({ context }) => {
// Prefetch auth or other global data
await context.queryClient.ensureQueryData(authQueryOptions)
},
})
function RootComponent() {
return (
<html>
<head>
<HeadContent />
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
)
}TanStack Start handles SSR and hydration automatically via the Vite plugin. No separate entry files needed.
Good Example: Testing with Mock QueryClient
// tests/posts.test.tsx
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { render } from '@testing-library/react'
function renderWithProviders(route: string) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
})
const router = createRouter({
routeTree,
context: { queryClient },
Wrap: ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
),
})
return {
...render(<RouterProvider router={router} />),
queryClient,
}
}
test('loads posts', async () => {
const { queryClient } = renderWithProviders('/posts')
// Pre-populate cache for testing
queryClient.setQueryData(['posts'], mockPosts)
// ... assertions
})Context
- Router context flows to all loaders and beforeLoad hooks
- Creating QueryClient per request is essential for SSR
- Use
setupRouterSsrQueryIntegrationfor automatic SSR handling - Access queryClient via
contextparameter in loaders - This pattern enables clean dependency injection for testing
- Install:
npm install @tanstack/react-router-ssr-query
ssr-dehydrate-hydrate: Configure SSR Query Integration
Priority: CRITICAL
Explanation
Use @tanstack/react-router-ssr-query to automatically handle SSR dehydration/hydration between TanStack Router and TanStack Query. This package automates cache transfer, streaming, and redirect handling.
Bad Example
// Manual dehydration - verbose and error-prone
import { dehydrate, hydrate } from '@tanstack/react-query'
const router = createRouter({
routeTree,
context: { queryClient },
// Manual approach - lots of boilerplate
dehydrate: () => ({
queryClientState: dehydrate(queryClient),
}),
hydrate: (dehydrated) => {
hydrate(queryClient, dehydrated.queryClientState)
},
Wrap: ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
),
})Good Example: Modern SSR Integration
// 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 getRouter() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
staleTime: 1000 * 60 * 2, // 2 minutes
},
},
})
const router = createRouter({
routeTree,
context: { queryClient },
defaultPreload: 'intent',
defaultPreloadStaleTime: 0, // Let Query manage cache freshness
scrollRestoration: true,
defaultStructuralSharing: true,
})
// Automatic SSR dehydration/hydration
setupRouterSsrQueryIntegration({
router,
queryClient,
handleRedirects: true, // Intercept redirects from queries/mutations
wrapQueryClient: true, // Auto-wrap with QueryClientProvider
})
return router
}Good Example: With Error and NotFound Components
import { DefaultCatchBoundary } from '@/components/DefaultCatchBoundary'
import { DefaultNotFound } from '@/components/DefaultNotFound'
export function getRouter() {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
staleTime: 1000 * 60 * 2,
},
},
})
const router = createRouter({
routeTree,
context: { queryClient, user: null },
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
defaultErrorComponent: DefaultCatchBoundary,
defaultNotFoundComponent: DefaultNotFound,
scrollRestoration: true,
defaultStructuralSharing: true,
})
setupRouterSsrQueryIntegration({
router,
queryClient,
handleRedirects: true,
wrapQueryClient: true,
})
return router
}Good Example: Custom QueryClientProvider
// If you need custom provider setup (e.g., for DevTools)
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
export function getRouter() {
const queryClient = new QueryClient()
const router = createRouter({
routeTree,
context: { queryClient },
defaultPreload: 'intent',
defaultPreloadStaleTime: 0,
scrollRestoration: true,
})
setupRouterSsrQueryIntegration({
router,
queryClient,
handleRedirects: true,
wrapQueryClient: false, // We'll provide our own
})
// Custom wrapper with DevTools
router.options.Wrap = ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
{process.env.NODE_ENV === 'development' && (
<ReactQueryDevtools initialIsOpen={false} />
)}
</QueryClientProvider>
)
return router
}Good Example: Vite Configuration
// vite.config.ts
import { tanstackStart } from "@tanstack/start/plugin/vite"
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
export default defineConfig({
plugins: [
tanstackStart(), // Handles SSR entry points automatically
react(),
],
})TanStack Start handles client hydration and SSR automatically via the Vite plugin. No separate client.tsx or ssr.tsx files are needed.
setupRouterSsrQueryIntegration Options
| Option | Type | Default | Description |
|---|---|---|---|
router | Router | Required | Your router instance |
queryClient | QueryClient | Required | Your QueryClient instance |
handleRedirects | boolean | true | Intercept and handle redirects from queries/mutations |
wrapQueryClient | boolean | true | Wrap router with QueryClientProvider automatically |
SSR Data Flow
Server:
1. Request received
2. getRouter() creates fresh QueryClient + Router
3. setupRouterSsrQueryIntegration connects them
4. Router matches routes, runs loaders
5. Loaders call ensureQueryData → data cached
6. Integration auto-dehydrates QueryClient state
7. HTML + serialized state streamed to client
Client:
1. HTML rendered (React hydrates)
2. getRouter() creates fresh QueryClient + Router
3. Integration auto-hydrates state from server
4. useSuspenseQuery finds data in cache - no refetch!
5. App is interactive with data already loadedContext
- Install:
npm install @tanstack/react-router-ssr-query - Creates fresh QueryClient per request (required for SSR)
- Handles streaming of queries that resolve during render
- Set
defaultPreloadStaleTime: 0to let Query manage freshness - Each SSR request needs its own router instance via
getRouter() - The integration handles all dehydration/hydration automatically
Related skills
How it compares
Pick tanstack-integration-best-practices when Router and Query run together; use Query-only or Router-only guides when not combining both libraries.
FAQ
What is tanstack-integration-best-practices?
Best practices for integrating TanStack Query with TanStack Router and TanStack Start. Patterns for full-stack data flow, SSR, and caching coordination.
When should I use tanstack-integration-best-practices?
Best practices for integrating TanStack Query with TanStack Router and TanStack Start. Patterns for full-stack data flow, SSR, and caching coordination.
Is tanstack-integration-best-practices safe to install?
Review the Security Audits panel on this page before production use.