# Tanstack

> Expert assistance for TanStack Router, Query (v5), and Table (v8). Use when users need help with file-based routing, type-safe navigation, data loading, search params, queries, mutations, optimistic updates, infinite queries, table state management, or debugging TanStack libraries.

- Skill: `barisariburnu/tanstack` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add barisariburnu/tanstack`
- Raw SKILL.md: https://api.skillmd.com/api/skills/barisariburnu/tanstack/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: barisariburnu (https://skillmd.com/u/barisariburnu)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/barisariburnu/tanstack

---


# 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 `staleTime` vs `gcTime` (formerly `cacheTime`)
- **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 `columns` and `data` to 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:

```tsx
// 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

```tsx
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

```tsx
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

```tsx
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](references/router.md)**: File structure, loaders, navigation, route guards, error handling
- **[Query Reference](references/query.md)**: Query keys, caching, mutations, infinite queries, suspense
- **[Table Reference](references/table.md)**: 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()` and `Route.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 `columns` array on every render—use `useMemo` or define outside component
- **Do** provide stable `data` reference (from Query cache, state, or `useMemo`)
- **Don't** expect styled components—Table is headless, you provide all UI

## Best Practices Checklist

- [ ] Use `queryOptions` factory for shared query configuration between loader and component
- [ ] Enable `strictNullChecks` in 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 `useSuspenseQuery` with Router loaders for automatic loading states
- [ ] Validate search params with Zod/Valibot for runtime safety
- [ ] Configure appropriate `staleTime` to reduce unnecessary refetches
- [ ] Use optimistic updates for mutations to improve perceived performance
- [ ] Leverage `gcTime` (garbage collection time) to control cache cleanup

