Skills tanstack
Build type-safe React apps with TanStack Query (data fetching, caching, mutations), Router (file-based routing, search params, loaders), and Start (SSR, server functions, middleware). Use when working with react-query, data fetching, server state, routing, search params, loaders, SSR, server functions, or full-stack React. Triggers on tanstack, react query, query client, useQuery, useMutation, invalidateQueries, tanstack router, file-based routing, search params, route loader, tanstack start, createServerFn, server functions, SSR.
git clone https://github.com/tenequm/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/tenequm/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/tanstack" ~/.claude/skills/tenequm-skills-tanstack && rm -rf "$T"
skills/tanstack/SKILL.mdTanStack (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 ( <QueryClientProvider client={queryClient}> <YourApp /> </QueryClientProvider> ) }
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<Todo[]> }, }) // In component - full type inference from queryOptions function TodoList() { const { data, isLoading, error } = useQuery(todosQueryOptions) if (isLoading) return <Spinner /> if (error) return <div>Error: {error.message}</div> return <ul>{data.map(t => <li key={t.id}>{t.title}</li>)}</ul> }
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 ( <button onClick={() => mutation.mutate({ title: 'New' })}> {mutation.isPending ? 'Creating...' : 'Create'} </button> ) }
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 <Suspense> 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 <ReactQueryDevtools initialIsOpen={false} />
Query Deep Dives
- Complete Query reference with all patternsquery-guide.md
- useInfiniteQuery, pagination, virtual scrollinfinite-queries.md
- Optimistic UI, rollback, undooptimistic-updates.md
- staleTime tuning, deduplication, prefetchingquery-performance.md
- Cache invalidation strategies, filters, predicatesquery-invalidation.md
- Type inference, generics, custom hooksquery-typescript.md
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 route (always rendered) | |
| Index route | -> |
| Dynamic segment | -> |
| Pathless layout | wraps children |
| Route group (no URL) | -> |
Type-Safe Navigation
<Link to="/posts/$postId" params={{ postId: '123' }}>View Post</Link> // Active styling <Link to="/posts" activeProps={{ className: 'font-bold' }}>Posts</Link> // 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 <Link from={Route.fullPath} search={prev => ({ ...prev, page: prev.page + 1 })}>Next</Link> }, })
Use
fallback(...).default(...) from the Zod adapter. Plain .catch() causes type loss.
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: () => <Spinner />, component: () => { const posts = Route.useLoaderData() return <PostList posts={posts} /> }, })
Route Context (Dependency Injection)
// __root.tsx interface RouterContext { queryClient: QueryClient } export const Route = createRootRouteWithContext<RouterContext>()({ 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
- Complete Router reference with all patternsrouter-guide.md
- Custom serialization, Standard Schema, sharing paramssearch-params.md
- Deferred loading, streaming SSR, shouldReloaddata-loading.md
- Virtual routes, route masking, navigation blockingrouting-patterns.md
- Automatic/manual splitting strategiescode-splitting.md
- SSR setup, streaming, hydrationrouter-ssr.md
TanStack Start (RC)
Full-stack framework extending Router with SSR, server functions, middleware. API stable, feature-complete. No RSC yet.
Setup
pnpm create @tanstack/start@latest
// 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' }) .inputValidator(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 <UserList users={users} /> }, })
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 }))
SSR Modes
| Mode | Use Case |
|---|---|
(default) | SEO, performance |
| Browser-only features |
| Dashboards (data on server, render on client) |
SPA mode:
tanstackStart({ spa: { enabled: true } }) in vite.config.ts
Deployment
- Cloudflare Workers:
(official partner)@cloudflare/vite-plugin - Netlify:
@netlify/vite-plugin-tanstack-start - Node/Vercel/Bun/Docker: via Nitro
- Static:
tanstackStart({ prerender: { enabled: true, crawlLinks: true } })
Start Deep Dives
- Complete Start reference with all patternsstart-guide.md
- Streaming, FormData, progressive enhancementserver-functions.md
- sendContext, custom fetch, global configmiddleware.md
- Selective SSR, shellComponent, fallback renderingssr-modes.md
- Dynamic params, wildcards, pathless layoutsserver-routes.md
Best Practices
- Use
factory for reusable, type-safe query definitionsqueryOptions() - Structure query keys hierarchically -
['entity', 'action', { filters }] - Set staleTime per data type - static:
, dynamic:Infinity
, moderate:05min - Always validate search params with Zod via
+zodValidatorfallback().default() - Provide
on navigation - narrows types, catches route mismatchesfrom - Use route context for DI - pass QueryClient, auth via
createRootRouteWithContext - Set
globally for perceived performancedefaultPreload: 'intent' - Never put secrets in loaders - use
for server-only codecreateServerFn() - Compose middleware hierarchically - global -> route -> function
- Use
on every content route for SEO (title, description, OG tags)head()
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