TanStack Cheat Sheet π₯
- Package Directory:
- Query π€:
- React NPM: @tanstack/react-query
- Devtools NPM: @tanstack/react-query-devtools
- Router π§:
- React NPM: @tanstack/react-router
- Devtools NPM: @tanstack/router-devtools
- Table π§Ύ:
- React NPM: @tanstack/react-table
- Virtual π:
- React NPM: @tanstack/react-virtual
π€ TanStack Query (React Query)
- Mental Model:
- Query: Cached async result for a queryKey.
- Mutation: Write action; usually followed by cache update and/or invalidation.
- staleTime: How long data is considered fresh.
- gcTime: How long inactive queries stay in cache before garbage collection.
- Defaults & Gotchas:
- Inactive queries: Garbage-collected after 5 min by default (tune via gcTime).
- Failed queries: Retry 3 times by default (tune via retry/retryDelay).
- Structural sharing: Enabled by default for JSON-compatible data to maintain memo stability.
- Client & Devtools Setup:
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient({
defaultOptions: {
queries: { staleTime: 60000, gcTime: 1800000, retry: 2, refetchOnWindowFocus: true },
mutations: { retry: 0 }
}
})
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
)
}
- Query Keys:
- Rules: Always use arrays; entities first, then serializable parameters; keep stable.
- Factory pattern:
export const qk = {
posts: () => ['posts'] as const,
post: (id: string) => ['posts', id] as const,
postComments: (id: string) => ['posts', id, 'comments'] as const,
searchPosts: (q: string, page: number) => ['posts', 'search', { q, page }] as const,
}
- Query & Mutation Basics:
- Options: enabled (dependent queries), select (derive/memoize data), placeholderData (previous page data during transition), refetchOnReconnect, refetchInterval.
- Usage:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
function Posts() {
const { data, isPending, error } = useQuery({
queryKey: qk.posts(),
queryFn: () => fetch('/api/posts').then(r => r.json()),
})
if (isPending) return <div>Loadingβ¦</div>
if (error) return <div>Error: {error.message}</div>
return data.map((p: any) => <div key={p.id}>{p.title}</div>)
}
function NewPost() {
const queryClient = useQueryClient()
const create = useMutation({
mutationFn: (body: { title: string }) =>
fetch('/api/posts', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
}).then(r => r.json()),
onSuccess: () => queryClient.invalidateQueries({ queryKey: qk.posts() }),
})
return <button onClick={() => create.mutate({ title: 'Hello' })}>Create</button>
}
- Cache Actions:
- Invalidate (marks stale, refetches active): queryClient.invalidateQueries({ queryKey: qk.posts() })
- Refetch (immediate run): queryClient.refetchQueries({ queryKey: qk.posts(), type: βactiveβ })
- Prefetch (instant navigation): queryClient.prefetchQuery({ queryKey: qk.post(id), queryFn: fetchFn, staleTime: 300000 })
- Cache update: Direct update on queryClient.setQueryData when server result is known or invalidation is too expensive.
- Optimistic Updates:
const mutation = useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: qk.post(newTodo.id) })
const previous = queryClient.getQueryData(qk.post(newTodo.id))
queryClient.setQueryData(qk.post(newTodo.id), newTodo)
return { previous }
},
onError: (_err, newTodo, ctx) => {
queryClient.setQueryData(qk.post(newTodo.id), ctx?.previous)
},
onSettled: (_data, _err, newTodo) => {
queryClient.invalidateQueries({ queryKey: qk.post(newTodo.id) })
},
})
- Pagination & Infinite Queries:
- Pagination: Include page in queryKey, use placeholderData: (prev) => prev.
- Infinite: Include cursor/pageParam in queryKey, implement getNextPageParam.
- Testing Tips:
- Fresh client: Create a new QueryClient per test.
- Determinism: Disable retries (retry: false).
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
π§ TanStack Router
- Capabilities: Fully type-safe routing (params and search params), nested layouts via Outlet, loaders + beforeLoad for data/guards, Query integration.
- Router Cache Defaults:
- staleTime: Defaults to 0 (routes considered stale, will reload on rematch).
- gcTime: Defaults to 30 min.
- preloadStaleTime: Preloaded routes treated as fresh for 30 sec.
- router.invalidate(): Forces active loaders to reload.
- Setup Pattern:
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'
import { routeTree } from './routeTree.gen'
export type RouterContext = { queryClient: QueryClient; auth: { getUser: () => Promise<any> } }
export const router = createRouter({ routeTree, context: { queryClient, auth } as RouterContext })
export function App() {
return (
<>
<RouterProvider router={router} />
<TanStackRouterDevtools position="bottom-right" />
</>
)
}
- Search Params:
import { z } from 'zod'
import { zodValidator, fallback } from '@tanstack/zod-adapter'
const searchSchema = z.object({
q: fallback(z.string(), ''),
page: fallback(z.number().int().min(1), 1),
})
export const Route = createFileRoute('/posts')({
validateSearch: zodValidator(searchSchema),
})
- Loader Integration (Preferred Query Flow):
- LoaderDeps: Map inputs that trigger loader execution.
- ensureQueryData: Call inside loader to load cache, read with useSuspenseQuery in component.
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'
const postsQuery = queryOptions({
queryKey: qk.posts(),
queryFn: () => fetch('/api/posts').then(r => r.json()),
})
export const Route = createFileRoute('/posts')({
loader: ({ context }) => context.queryClient.ensureQueryData(postsQuery),
component: Posts,
})
function Posts() {
const { data } = useSuspenseQuery(postsQuery)
return data.map((p: any) => <div key={p.id}>{p.title}</div>)
}
- Guards & Redirects:
- beforeLoad: Use for auth checks, redirecting, and enriching route context.
π§Ύ TanStack Table
- Mental Model: Headless architecture; provides row models, state, and helpers. Bring your own markup and styles.
- Setup:
import { flexRender, getCoreRowModel, useReactTable, type ColumnDef } from '@tanstack/react-table'
type Person = { firstName: string; lastName: string; age: number }
const columns: ColumnDef<Person>[] = [
{ accessorKey: 'firstName', header: 'First Name' },
{ accessorKey: 'lastName', header: 'Last Name' },
{ accessorKey: 'age', header: 'Age' }
]
export function PeopleTable({ data }: { data: Person[] }) {
const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel() })
return (
<table>
<thead>
{table.getHeaderGroups().map(hg => (
<tr key={hg.id}>
{hg.headers.map(h => <th key={h.id}>{h.isPlaceholder ? null : flexRender(h.column.columnDef.header, h.getContext())}</th>)}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id}>
{row.getVisibleCells().map(c => <td key={c.id}>{flexRender(c.column.columnDef.cell, c.getContext())}</td>)}
</tr>
))}
</tbody>
</table>
)
}
- Row Processing Features:
- Client-side: Controlled state sorting/filtering/pagination via getSortedRowModel, getFilteredRowModel, getPaginationRowModel.
const [sorting, setSorting] = React.useState<SortingState>([])
const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 20 })
const table = useReactTable({
data, columns, state: { sorting, pagination },
onSortingChange: setSorting, onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
})
- Server-side sorting/pagination:
- Configuration: Set manualSorting: true, manualPagination: true.
- Setup: Feed sorting/pagination state into API queryKey (caching handled by TanStack Query).
- Pagination gotchas:
- autoResetPageIndex: Defaults to true (pageIndex resets on data/filter updates). manualPagination automatically disables this.
- State collision: Never pass pagination to both state and initialState (state takes priority).
- Virtualization (Big Tables): Headless pair with TanStack Virtual; virtualize rows while keeping headers sticky.
π§ βοΈ Architecture
- HTTP: Single client/API module.
- Queries: Dedicated Query Key factory module + queryOptions.
- Structure: Features folders containing routes, queries, and components.
- Validation: Zod for route params and API payloads.
π Resources