From tenequm-skills
Builds type-safe React apps with TanStack Query for server state, Router for file-based routing, and Start for SSR/server functions. Covers setup, queries, mutations, routing patterns.
How this skill is triggered — by the user, by Claude, or both
Slash command
/tenequm-skills:tanstackThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
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.
CHANGELOG.mdLICENSE.txtreferences/code-splitting.mdreferences/data-loading.mdreferences/infinite-queries.mdreferences/middleware.mdreferences/optimistic-updates.mdreferences/query-guide.mdreferences/query-invalidation.mdreferences/query-performance.mdreferences/query-typescript.mdreferences/router-guide.mdreferences/router-ssr.mdreferences/routing-patterns.mdreferences/search-params.mdreferences/server-functions.mdreferences/server-routes.mdreferences/ssr-modes.mdreferences/start-guide.mdType-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.
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:
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>
)
}
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>
}
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>
)
}
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' }),
}))
pnpm add @tanstack/react-query-devtools
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
// Add inside QueryClientProvider
<ReactQueryDevtools initialIsOpen={false} />
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 hookspnpm 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 }
}
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 |
<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.
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 (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.
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} />
},
})
// __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-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, hydrationFull-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.
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()
],
})
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 <UserList users={users} />
},
})
Critical: Loaders are isomorphic (run on server AND client). Never put secrets in loaders - use createServerFn() instead.
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]
| 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
@cloudflare/vite-plugin), Netlify (@netlify/vite-plugin-tanstack-start), RailwaytanstackStart({ prerender: { enabled: true, crawlLinks: true } })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 layoutsqueryOptions() factory for reusable, type-safe query definitions['entity', 'action', { filters }]Infinity, dynamic: 0, moderate: 5minzodValidator + fallback().default()from on navigation - narrows types, catches route mismatchescreateRootRouteWithContextdefaultPreload: 'intent' globally for perceived performancebeforeLoad/route guards are UX, not the security boundary. Never put secrets in isomorphic loaders - use createServerFn()head() on every content route for SEO (title, description, OG tags)npx claudepluginhub tenequm/skillsGuides setup, configuration, and usage of TanStack Start full-stack React framework with SSR, streaming, server functions, routes, middleware, and Vite. For app creation, deployment, auth, Tailwind integration, and Next.js migrations.
Builds full-stack React apps with TanStack Start featuring per-route SSR/CSR/SSG, file-based routing, server functions, and Cloudflare Workers support. Use for Next.js migrations, edge rendering, or hydration issues.
Manages server state with automatic caching, background refetching, pagination, infinite scrolling, and optimistic updates for React, Vue, Svelte, Angular apps.