Help us improve
Share bugs, ideas, or general feedback.
From flow
Provides code patterns, quick references, and examples for TanStack libraries including React Query for data fetching/mutations, TanStack Router for file-based routing, TanStack Table, and Zod validation in React/TypeScript apps.
npx claudepluginhub cofin/flow --plugin flowHow this skill is triggered — by the user, by Claude, or both
Slash command
/flow:tanstackThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The TanStack ecosystem provides standard libraries for modern React/TypeScript applications, emphasizing type safety, performance, and developer experience.
TanStack Router - 100% type-safe routing, file-based routes, loaders, search params. Use when implementing routing in React apps (NOT Next.js).
Provides expertise in TanStack Query for React/Next.js data fetching, caching (staleTime/gcTime), mutations, optimistic updates, cache invalidation, and App Router SSR hydration.
Guides TanStack Router setup in React: file-based routing, type-safe search/path params, data loaders, auth protection, code splitting, SSR, error handling, testing, and bundler config.
Share bugs, ideas, or general feedback.
The TanStack ecosystem provides standard libraries for modern React/TypeScript applications, emphasizing type safety, performance, and developer experience.
import { queryOptions, useQuery } from '@tanstack/react-query'
// Define query options as a factory -- reusable across components and loaders
export const usersQueryOptions = (filters?: UserFilters) =>
queryOptions({
queryKey: ['users', filters],
queryFn: () => api.getUsers(filters),
staleTime: 5 * 60 * 1000, // 5 minutes
})
function UsersPage() {
const { data, isLoading, error } = useQuery(usersQueryOptions())
if (isLoading) return <Spinner />
if (error) return <ErrorMessage error={error} />
return <UserList users={data} />
}
export function useCreateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: UserCreate) => api.createUser(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
}
src/routes/
├── __root.tsx # Root layout
├── index.tsx # / route
├── _layout.tsx # Layout wrapper (no URL segment)
├── users/
│ ├── index.tsx # /users
│ ├── $userId.tsx # /users/:userId
│ └── $userId.edit.tsx # /users/:userId/edit
import { createFileRoute } from '@tanstack/react-router'
import { queryClient } from '@/lib/query-client'
export const Route = createFileRoute('/users')({
loader: () => queryClient.ensureQueryData(usersQueryOptions()),
component: UsersPage,
})
// Route parameters
export const Route = createFileRoute('/users/$userId')({
loader: ({ params }) =>
queryClient.ensureQueryData(userQueryOptions(params.userId)),
component: UserDetailPage,
})
import { z } from 'zod'
const searchSchema = z.object({
page: z.number().default(1),
sort: z.enum(['name', 'date']).default('name'),
})
export const Route = createFileRoute('/users')({
validateSearch: searchSchema,
component: UsersPage,
})
import { useReactTable, getCoreRowModel, flexRender, ColumnDef } from '@tanstack/react-table'
const columns: ColumnDef<User>[] = [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
{
accessorKey: 'createdAt',
header: 'Joined',
cell: (info) => new Date(info.getValue<string>()).toLocaleDateString(),
},
]
function UsersTable({ users }: { users: User[] }) {
const table = useReactTable({
data: users,
columns,
getCoreRowModel: getCoreRowModel(),
})
// render with flexRender -- see references/table.md
}
| Need | Library | Key Import |
|---|---|---|
| Data fetching & caching | TanStack Query | @tanstack/react-query |
| Client-side routing | TanStack Router | @tanstack/react-router |
| Table / data grid | TanStack Table | @tanstack/react-table |
| Form state & validation | TanStack Form | @tanstack/react-form |
| Lightweight state | TanStack Store | @tanstack/store |
queryOptions() -- always set staleTimecreateFileRoute -- pre-fetch with loaderColumnDef[] typed to your data -- use getCoreRowModel() as baseuseForm() with Zod adapter for validation@/lib/queries/)ensureQueryData in route loaders for data pre-fetchinguseQuery for cache hitsqueryClient.prefetchQuery() for navigation linksRun through the validation checkpoint below before considering the work complete.
staleTime on queries -- the default (0) causes unnecessary refetches on every mountqueryKey arrays -- include all variables the query depends on: ['users', filters]queryOptions() factory -- makes query keys reusable across components and loadersisLoading, error from useQuery must be checkedqueryClient.prefetchQuery() in onMouseEnterqueryClient.setQueryData() for optimistic updates<Link> components or hooks between themBefore delivering TanStack code, verify:
useQuery calls have staleTime set (via queryOptions factory or directly)ensureQueryData (not fetchQuery) to leverage cacheColumnDef<T>[]Task: "Create a users list page with TanStack Router + Query, including search, pagination, and prefetch on hover."
// --- lib/queries/users.ts ---
import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api'
interface UserFilters {
search?: string
page?: number
}
export const usersQueryOptions = (filters: UserFilters = {}) =>
queryOptions({
queryKey: ['users', filters],
queryFn: () => api.getUsers(filters),
staleTime: 5 * 60 * 1000,
})
export const userQueryOptions = (userId: string) =>
queryOptions({
queryKey: ['users', userId],
queryFn: () => api.getUser(userId),
staleTime: 5 * 60 * 1000,
})
// --- routes/users/index.tsx ---
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
import { usersQueryOptions } from '@/lib/queries/users'
import { queryClient } from '@/lib/query-client'
const searchSchema = z.object({
search: z.string().optional(),
page: z.number().default(1),
})
export const Route = createFileRoute('/users/')({
validateSearch: searchSchema,
loader: ({ search }) =>
queryClient.ensureQueryData(usersQueryOptions(search)),
component: UsersPage,
})
function UsersPage() {
const { search, page } = Route.useSearch()
const navigate = Route.useNavigate()
const { data, isLoading, error } = useQuery(
usersQueryOptions({ search, page }),
)
if (isLoading) return <Spinner />
if (error) return <ErrorMessage error={error} />
return (
<div>
<SearchInput
value={search ?? ''}
onChange={(value) => navigate({ search: { search: value, page: 1 } })}
/>
<UserList users={data.items} />
<Pagination
page={page}
totalPages={data.totalPages}
onPageChange={(p) => navigate({ search: { search, page: p } })}
/>
</div>
)
}
// --- components/UserLink.tsx ---
import { Link } from '@tanstack/react-router'
import { useQueryClient } from '@tanstack/react-query'
import { userQueryOptions } from '@/lib/queries/users'
function UserLink({ userId, name }: { userId: string; name: string }) {
const queryClient = useQueryClient()
return (
<Link
to="/users/$userId"
params={{ userId }}
onMouseEnter={() => {
queryClient.prefetchQuery(userQueryOptions(userId))
}}
>
{name}
</Link>
)
}
For detailed guides and code examples, refer to the following documents in references/: