TanStack
Expert guidance for the TanStack ecosystem: Router (type-safe routing), Query v5 (server state management), and Table v8 (headless tables).
When to Use This Skill
Use this skill when users ask about:
- TanStack Router: File-based routing, route configuration, loaders, type-safe navigation, search params with validation, nested routes, route guards
- TanStack Query v5: Data fetching, caching strategies, query keys, mutations, optimistic updates, infinite queries, query invalidation, suspense
- TanStack Table v8: Table setup, column definitions, sorting, filtering, pagination, grouping, headless UI patterns
- Integration: Combining Router + Query for optimal data loading and caching
Core Principles
Router
- Type-Safety First: Leverage TypeScript inference for params, search, and context
- File-Based Routing: Use naming conventions (
__root.tsx,index.tsx,$param.tsx) - External Caching: Prefer TanStack Query integration over built-in caching
- Search Params as State: Treat search params as type-safe global state
Query v5
- Object Syntax: Always use
useQuery({ queryKey, queryFn })(not deprecated array syntax) - Query Keys: Structure hierarchically
['resource', id, filters]for better cache management - Stale-While-Revalidate: Understand
staleTimevsgcTime(formerlycacheTime) - Optimistic Updates: Use mutations with cache updates for instant UI feedback
Table v8
- Headless UI: Table provides logic/state only—you control all markup and styling
- Stable References: Memoize
columnsanddatato prevent unnecessary re-renders - Feature Composition: Enable only needed features (sorting, filtering, etc.)
Quick Start Patterns
1. Router + Query Integration (Recommended)
Combine for type-safe, cached data loading:
// queries/posts.ts
import { queryOptions } from '@tanstack/react-query'
export const postQueryOptions = (postId: string) => queryOptions({
queryKey: ['posts', postId],
queryFn: async () => {
const res = await fetch(`/api/posts/${postId}`)
if (!res.ok) throw new Error('Failed to fetch post')
return res.json()
},
staleTime: 5 * 60 * 1000, // 5 minutes
})
// routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
import { useSuspenseQuery } from '@tanstack/react-query'
import { postQueryOptions } from '../queries/posts'
export const Route = createFileRoute('/posts/$postId')({
loader: ({ context: { queryClient }, params: { postId } }) =>
queryClient.ensureQueryData(postQueryOptions(postId)),
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
const { data } = useSuspenseQuery(postQueryOptions(postId))
return <div>{data.title}</div>
}
2. Type-Safe Search Params with Validation
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'
const searchSchema = z.object({
page: z.number().int().positive().default(1),
sortBy: z.enum(['date', 'title', 'author']).default('date'),
filter: z.string().optional(),
})
export const Route = createFileRoute('/posts/')({
validateSearch: (search) => searchSchema.parse(search),
component: PostsComponent,
})
function PostsComponent() {
const search = Route.useSearch()
const navigate = Route.useNavigate()
// Type-safe: search.page, search.sortBy, search.filter
const updatePage = (newPage: number) => {
navigate({ search: (prev) => ({ ...prev, page: newPage }) })
}
return <div>Page: {search.page}</div>
}
3. Mutations with Optimistic Updates
import { useMutation, useQueryClient } from '@tanstack/react-query'
function useUpdatePost() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (data: { id: string; title: string }) => {
const res = await fetch(`/api/posts/${data.id}`, {
method: 'PATCH',
body: JSON.stringify(data),
})
return res.json()
},
onMutate: async (newData) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['posts', newData.id] })
// Snapshot previous value
const previous = queryClient.getQueryData(['posts', newData.id])
// Optimistically update
queryClient.setQueryData(['posts', newData.id], newData)
return { previous }
},
onError: (err, newData, context) => {
// Rollback on error
queryClient.setQueryData(['posts', newData.id], context?.previous)
},
onSettled: (data, error, variables) => {
// Refetch after success or error
queryClient.invalidateQueries({ queryKey: ['posts', variables.id] })
},
})
}
4. Basic Table Setup
import { useReactTable, getCoreRowModel, flexRender, createColumnHelper } from '@tanstack/react-table'
type User = {
id: string
name: string
email: string
}
const columnHelper = createColumnHelper<User>()
const columns = [
columnHelper.accessor('id', {
header: 'ID',
cell: (info) => info.getValue(),
}),
columnHelper.accessor('name', {
header: 'Name',
}),
columnHelper.accessor('email', {
header: 'Email',
}),
]
function UsersTable({ data }: { data: User[] }) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id}>
{flexRender(header.column.columnDef.header, header.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
)
}
Reference Documentation
For comprehensive guides and advanced patterns:
- Router Reference: File structure, loaders, navigation, route guards, error handling
- Query Reference: Query keys, caching, mutations, infinite queries, suspense
- Table Reference: Column types, sorting, filtering, pagination, custom features
Common Pitfalls & Solutions
Router
- Don't manually type
<Link to="...">props—let Router infer from route tree - Do use
Route.useParams()andRoute.useSearch()for type safety within components - Don't fetch data in loaders without external caching—use Query's
ensureQueryData
Query
- Don't use array syntax:
useQuery(['key'], fn) - Do use object syntax:
useQuery({ queryKey: ['key'], queryFn: fn }) - Don't put functions in query keys—use stable identifiers only
- Do structure keys hierarchically:
['posts', postId, { filter, sort }]
Table
- Don't recreate
columnsarray on every render—useuseMemoor define outside component - Do provide stable
datareference (from Query cache, state, oruseMemo) - Don't expect styled components—Table is headless, you provide all UI
Best Practices Checklist
- Use
queryOptionsfactory for shared query configuration between loader and component - Enable
strictNullChecksin TypeScript for full Router type safety - Structure query keys hierarchically for efficient invalidation
- Implement error boundaries for route-level error handling
- Memoize table columns and data for performance
- Use
useSuspenseQuerywith Router loaders for automatic loading states - Validate search params with Zod/Valibot for runtime safety
- Configure appropriate
staleTimeto reduce unnecessary refetches - Use optimistic updates for mutations to improve perceived performance
- Leverage
gcTime(garbage collection time) to control cache cleanup