Write, debug, and tune Playwright specs with locator strategy, trace diagnosis, and CI-aware timeouts.
Data & analysis
tanstack
Try itReference patterns for TanStack Query, Router, and Start in full-stack React apps.
What it does
Reference patterns for TanStack Query (server state, caching, mutations, optimistic updates, streamed AI responses), Router (file-based routes, Zod-validated search params, route loaders, type-safe navigation), and Start (SSR, server functions, middleware, deployment). Includes code examples for queryOptions factories, route context DI, middleware composition, and deployment to Cloudflare, Netlify, Node, or Vercel.
When to use it
- Building a client-only SPA with API calls and file-based routes
- Adding type-safe SSR with server functions and middleware
- Validating URL search params with Zod and binding them to route loaders
- Streaming AI chat responses with experimental streamedQuery
The skill document
TanStack (Query + Router + Start)
Type-safe libraries for React applications. Query manages server state (fetching, caching, mutations). Router provides file-based routing with validated search params and data loaders. Start extends Router with SSR, server functions, and middleware for full-stack apps.
When to Use
Query - data fetching, caching, mutations, optimistic updates, infinite scroll, streaming AI/SSE responses, tRPC v11 integration Router - file-based routing, type-safe navigation, validated search params, route loaders, code splitting, preloading Start - SSR/SSG, server functions (type-safe RPCs), middleware, API routes, deployment to Cloudflare/Vercel/Node
Decision tree:
- Client-only SPA with API calls -> Router + Query
- Full-stack with SSR/server functions -> Start + Query (Start includes Router)
TanStack Query v5
Setup
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
},
},
})
function App() {
return (
)
}
Queries
import { useQuery, queryOptions } from '@tanstack/react-query'
// Reusable query definition (recommended pattern)
const todosQueryOptions = queryOptions({
queryKey: ['todos'],
queryFn: async () => {
const res = await fetch('/api/todos')
if (!res.ok) throw new Error('Failed to fetch')
return res.json() as Promise
},
})
// In component - full type inference from queryOptions
function TodoList() {
const { data, isLoading, error } = useQuery(todosQueryOptions)
if (isLoading) return
if (error) return Error: {error.message}
return {data.map(t => {t.title})}
}
Mutations
import { useMutation, useQueryClient } from '@tanstack/react-query'
function CreateTodo() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (newTodo: { title: string }) =>
fetch('/api/todos', { method: 'POST', body: JSON.stringify(newTodo) }).then(r => r.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
mutation.mutate({ title: 'New' })}>
{mutation.isPending ? 'Creating...' : 'Create'}
)
}
Key Patterns
Query keys - hierarchical arrays for cache management:
['todos'] // all todos
['todos', 'list', { page, sort }] // filtered list
['todo', todoId] // single item
Dependent queries - chain with enabled:
const { data: user } = useQuery({ queryKey: ['user', id], queryFn: () => fetchUser(id) })
const { data: projects } = useQuery({
queryKey: ['projects', user?.id],
queryFn: () => fetchProjects(user!.id),
enabled: !!user?.id,
})
Important defaults: staleTime: 0, gcTime: 5min, retry: 3, refetchOnWindowFocus: true
Suspense - use useSuspenseQuery with `` boundaries
Streamed queries (experimental) - for AI chat/SSE:
import { experimental_streamedQuery as streamedQuery } from '@tanstack/react-query'
const { data: chunks } = useQuery(queryOptions({
queryKey: ['chat', sessionId],
queryFn: streamedQuery({ streamFn: () => fetchChatStream(sessionId), refetchMode: 'reset' }),
}))
DevTools
pnpm add @tanstack/react-query-devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
// Add inside QueryClientProvider
Query Deep Dives
query-guide.md- Complete Query reference with all patternsinfinite-queries.md- useInfiniteQuery, pagination, virtual scrolloptimistic-updates.md- Optimistic UI, rollback, undoquery-performance.md- staleTime tuning, deduplication, prefetchingquery-invalidation.md- Cache invalidation strategies, filters, predicatesquery-typescript.md- Type inference, generics, custom hooks
TanStack Router v1
Setup (Vite)
pnpm add @tanstack/react-router @tanstack/router-plugin
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
tanstackRouter({ autoCodeSplitting: true }),
react(),
],
})
// src/router.ts
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export const router = createRouter({ routeTree, defaultPreload: 'intent' })
declare module '@tanstack/react-router' {
interface Register { router: typeof router }
}
File-Based Routing
Files in src/routes/ auto-generate route config:
| Convention | Purpose | Example |
|---|---|---|
__root.tsx | Root route (always rendered) | src/routes/__root.tsx |
index.tsx | Index route | src/routes/index.tsx -> / |
$param | Dynamic segment | posts.$postId.tsx -> /posts/:id |
_prefix | Pathless layout | _layout.tsx wraps children |
(folder) | Route group (no URL) | (auth)/login.tsx -> /login |
Type-Safe Navigation
View Post
// Active styling
Posts
// Imperative
const navigate = useNavigate({ from: '/posts' })
navigate({ to: '/posts/$postId', params: { postId: post.id } })
Always provide from on Link and hooks - narrows types and improves TS performance.
Search Params
import { zodValidator, fallback } from '@tanstack/zod-adapter'
import { z } from 'zod'
const searchSchema = z.object({
page: fallback(z.number(), 1).default(1),
sort: fallback(z.enum(['newest', 'oldest']), 'newest').default('newest'),
})
export const Route = createFileRoute('/products')({
validateSearch: zodValidator(searchSchema),
component: () => {
const { page, sort } = Route.useSearch()
// Writing
return ({ ...prev, page: prev.page + 1 })}>Next
},
})
Use fallback(...).default(...) from the Zod adapter (Zod v3); plain .catch() causes type loss. With Zod v4 the adapter is no longer needed - pass the schema directly to validateSearch, and .catch() retains type inference.
Data Loading
export const Route = createFileRoute('/posts')({
// loaderDeps: only extract what loader needs (not full search)
loaderDeps: ({ search: { page } }) => ({ page }),
loader: ({ deps: { page } }) => fetchPosts({ page }),
pendingComponent: () => ,
component: () => {
const posts = Route.useLoaderData()
return
},
})
Route Context (Dependency Injection)
// __root.tsx
interface RouterContext { queryClient: QueryClient }
export const Route = createRootRouteWithContext()({ component: Root })
// router.ts
const router = createRouter({ routeTree, context: { queryClient } })
// Child route - queryClient available in loader
export const Route = createFileRoute('/posts')({
loader: ({ context: { queryClient } }) =>
queryClient.ensureQueryData(postsQueryOptions()),
})
Router Deep Dives
router-guide.md- Complete Router reference with all patternssearch-params.md- Custom serialization, Standard Schema, sharing paramsdata-loading.md- Deferred loading, streaming SSR, shouldReloadrouting-patterns.md- Virtual routes, route masking, navigation blockingcode-splitting.md- Automatic/manual splitting strategiesrouter-ssr.md- SSR setup, streaming, hydration
TanStack Start
Full-stack framework extending Router with SSR, server functions, middleware. Pre-1.0 (API stable, feature-complete, preparing for 1.0). React Server Components are available as an experimental feature - opt in with tanstackStart({ rsc: { enabled: true } }) + @vitejs/plugin-rsc (requires React 19, Vite 7+). Vite is the default bundler; Rsbuild is also supported.
Setup
npx @tanstack/cli@latest create # or use TanStack Builder: https://tanstack.com/builder
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
tanstackStart(),
viteReact(), // MUST come after tanstackStart()
],
})
Server Functions
Type-safe RPCs. Server code extracted from client bundles at build time.
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
// GET - no input
export const getUsers = createServerFn({ method: 'GET' })
.handler(async () => db.users.findMany())
// POST - validated input
export const createUser = createServerFn({ method: 'POST' })
.validator(z.object({ name: z.string(), email: z.string().email() }))
.handler(async ({ data }) => db.users.create(data))
// Call from loader
export const Route = createFileRoute('/users')({
loader: () => getUsers(),
component: () => {
const users = Route.useLoaderData()
return
},
})
Critical: Loaders are isomorphic (run on server AND client). Never put secrets in loaders - use createServerFn() instead.
Middleware
import { createMiddleware } from '@tanstack/react-start'
const authMiddleware = createMiddleware({ type: 'function' })
.server(async ({ next }) => {
const user = await getCurrentUser()
if (!user) throw redirect({ to: '/login' })
return next({ context: { user } })
})
const getProfile = createServerFn()
.middleware([authMiddleware])
.handler(async ({ context }) => context.user) // typed
Global middleware via src/start.ts:
export const startInstance = createStart(() => ({
requestMiddleware: [logger], // all requests
functionMiddleware: [auth], // all server functions
}))
CSRF: Start auto-installs createCsrfMiddleware() for server functions only when there is no src/start.ts. Once you create src/start.ts, add it back explicitly, or non-GET server functions lose same-origin protection:
requestMiddleware: [createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === 'serverFn' }), logger]
SSR Modes
| Mode | Use Case |
|---|---|
true (default) | SEO, performance |
false | Browser-only features |
'data-only' | Dashboards (data on server, render on client) |
SPA mode: tanstackStart({ spa: { enabled: true } }) in vite.config.ts
Deployment
- Official partners: Cloudflare Workers (
@cloudflare/vite-plugin), Netlify (@netlify/vite-plugin-tanstack-start), Railway - Node/Vercel/Bun/Docker: via Nitro
- Static:
tanstackStart({ prerender: { enabled: true, crawlLinks: true } })
Start Deep Dives
start-guide.md- Complete Start reference with all patternsserver-functions.md- Streaming, FormData, progressive enhancementmiddleware.md- sendContext, custom fetch, global configssr-modes.md- Selective SSR, shellComponent, fallback renderingserver-routes.md- Dynamic params, wildcards, pathless layouts
Best Practices
- Use
queryOptions()factory for reusable, type-safe query definitions - Structure query keys hierarchically -
['entity', 'action', { filters }] - Set staleTime per data type - static:
Infinity, dynamic:0, moderate:5min - Always validate search params with Zod via
zodValidator+fallback().default() - Provide
fromon navigation - narrows types, catches route mismatches - Use route context for DI - pass QueryClient, auth via
createRootRouteWithContext - Set
defaultPreload: 'intent'globally for perceived performance - Enforce auth at the data boundary - authorize inside the server function, server route, or API endpoint that reads/writes private data;
beforeLoad/route guards are UX, not the security boundary. Never put secrets in isomorphic loaders - usecreateServerFn() - Compose middleware hierarchically - global -> route -> function
- Use
head()on every content route for SEO (title, description, OG tags)
Resources
- Query Docs: https://tanstack.com/query/latest/docs/framework/react/overview
- Router Docs: https://tanstack.com/router/latest/docs/framework/react/overview
- Start Docs: https://tanstack.com/start/latest/docs/framework/react/overview
- GitHub: https://github.com/TanStack/query | https://github.com/TanStack/router
- Discord: https://discord.gg/tanstack
Questions people ask
- When should I reach for Start vs Router + Query?
- Use Start for full-stack apps that need SSR or server functions. Use Router + Query for client-only SPAs that just call APIs. Start already includes Router, so you do not stack them separately.
- Where does auth actually need to live?
- Inside the server function, server route, or API endpoint that reads or writes private data. beforeLoad and route guards handle UX, not security. Loaders are isomorphic and ship to the client, so secrets belong in createServerFn() instead.
- Do loaders run on the server during SSR?
- Yes. Loaders are isomorphic - they run on the server during SSR and again on the client for navigation, so any code in a loader is bundled for the client. Use createServerFn() for server-only logic and secrets.
Related skills
Find why your productivity system keeps failing, then apply the smallest fix — capacity math, bottleneck routing, durable local notes.
Generate and edit Draw.io, Mermaid, and Excalidraw diagrams from natural language using a structured JSON spec.
Stores durable facts in a categorized, plain-markdown vault on disk, alongside your agent's built-in memory.
Fetch raw ad creative, app, ranking, and revenue data from AdMapix as structured JSON.
Join a video meeting as an AI bot with voice, avatar, and screenshare across four operating modes.
More from tenequm
Browse all skillsBuild HTTP 402 machine-to-machine payments with the MPP protocol across TypeScript, Python, and Rust SDKs.
Opinionated guide to building Agent Skills for any agent, covering layout, frontmatter, descriptions, and Claude Code specifics.
Pinned reference for Lance v11.0.0-beta.6 and the Rust/Python engine, with v10.0.0 as the stable line.
Build and deploy JavaScript, TypeScript, Python, or Rust code on Cloudflare's global edge network using the Wrangler CLI.
Build native macOS apps with Swift 6.3, SwiftUI, SwiftData, concurrency, and on-device AI.
Wire up Privy auth, embedded wallets, and agent payments (x402, MPP) in React or Node apps.