From tenequm-skills
Builds type-safe React apps with TanStack Query for data fetching/caching/mutations, Router for file-based routing/loaders/search params, and Start for SSR/server functions/middleware.
npx claudepluginhub tenequm/skills --plugin gh-cliThis skill uses the workspace's default tool permissions.
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.
LICENSE.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.mdSearches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
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.
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. Plain .catch() causes type loss.
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. API stable, feature-complete. No RSC yet.
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()
],
})
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.
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
}))
| 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 (official partner)@netlify/vite-plugin-tanstack-starttanstackStart({ 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 performancecreateServerFn() for server-only codehead() on every content route for SEO (title, description, OG tags)