TanStack is a family of open-source, type-safe JavaScript and TypeScript libraries for building web applications. It began as React Query and now covers data fetching, routing, a full-stack framework, tables, forms, virtualization, client-side databases, AI and more.
You don't adopt TanStack as a whole. Each library stands alone, so the real question is which ones solve a problem you actually have. My default answer for a React app: TanStack Query for server data, almost always; TanStack Router if you want type-safe routing in a single-page app; TanStack Start if you want a full-stack React or Solid framework; and Table, Form, Virtual, DB, Pacer or AI only when you hit the specific problem each one exists for.
What the libraries have in common
TanStack is the umbrella name for the libraries created by Tanner Linsley and maintained by a community of contributors. The name became widely known when React Query was renamed TanStack Query with version 4, because the library had grown beyond React. Everything is free and open source under the MIT license, funded by partners and sponsors rather than paid licences.
The libraries share a design philosophy. Type safety comes first: APIs are shaped so TypeScript can infer types end to end, from route params to table columns to form fields. Most are headless, meaning they hold logic and state but render no UI, so they fit whatever design system you already use. And most are framework-agnostic, with a core package holding the logic and thin adapters for React, Solid, Vue, Svelte or Angular. That is why you'll find TanStack Table inside Next.js apps and TanStack Query in plain Vite SPAs.
The libraries and where they stand
As of October 2026, the TanStack libraries page lists 19 libraries in five groups: framework, data and state, UI, UX, and tooling. The status labels below are the ones shown there; libraries with no label are presented as stable.
Library | What it solves | Status |
|---|
Start | Full-stack React and Solid framework built on TanStack Router | Stable (1.0 on October 13, 2026) |
Router | Fully type-safe routing for React and Solid | Stable |
Redact | A smaller browser runtime that replaces React and React DOM on the client | New (0.2.0, October 2026) |
Query | Fetching, caching and syncing server state | Stable (v5) |
DB | Reactive, client-first store with live queries and optimistic writes | Beta label; 1.0 RC1 announced October 29, 2026 |
AI | Provider-agnostic building blocks for AI features in TypeScript | 1.0 (October 23, 2026) |
Store | Framework-agnostic reactive store used inside other TanStack libraries | Alpha |
Table | Headless tables and data grids | Stable (v9 is the latest major) |
Form | Headless, type-safe form state and validation | Stable |
Charts | A chart grammar for building visualizations | 1.0 (October 5, 2026) |
Markdown | A serializable document model for docs and AI streams | No label |
Highlight | Web-first syntax highlighting with compact, themeable HTML | No label |
Virtual | Virtualizing long lists and grids | Stable |
Pacer | Debouncing, throttling, rate limiting, queuing and batching | Beta |
Hotkeys | Type-safe keyboard shortcuts with scopes and user rebinding | Beta |
Intent | Ships Agent Skills (instructions for AI coding agents) with npm packages | Alpha |
Devtools | One devtools panel for TanStack and custom tools | Alpha |
Config | Tooling for publishing and maintaining JavaScript packages | No label |
CLI | Project scaffolding and CLI toolkit | Alpha |
The list moves quickly. Ranger, the range-slider library, no longer appears on that page at all. Check it before you commit a long-lived project to anything marked alpha or beta.
TanStack Query: start here
Data that lives on a server behaves differently from local UI state. It goes stale, other users change it, and several components often need it at once. If you store API responses in useState or Redux, you end up writing caching, deduplication, refetching, loading flags and error handling yourself, and it is easy to get wrong.
TanStack Query treats server data as a cache keyed by a query key. It deduplicates requests, refetches stale data when the window regains focus, retries failures, and hands you loading and error state. A fetch plus a mutation that refreshes the list looks like this:
import {
QueryClient,
QueryClientProvider,
useQuery,
useMutation,
useQueryClient,
} from '@tanstack/react-query'
type Todo = { id: number; title: string; done: boolean }
const queryClient = new QueryClient()
export function App() {
return (
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
function Todos() {
const qc = useQueryClient()
const { data, isPending, error } = useQuery({
queryKey: ['todos'],
queryFn: async (): Promise<Todo[]> => {
const res = await fetch('/api/todos')
if (!res.ok) throw new Error('Failed to load todos')
return res.json()
},
staleTime: 30_000, // treat data as fresh for 30 seconds
})
const addTodo = useMutation({
mutationFn: (title: string) =>
fetch('/api/todos', { method: 'POST', body: JSON.stringify({ title }) }),
onSuccess: () => qc.invalidateQueries({ queryKey: ['todos'] }),
})
if (isPending) return <p>Loading...</p>
if (error) return <p>{error.message}</p>
return (
<>
<button onClick={() => addTodo.mutate('New task')}>Add</button>
<ul>{data.map((t) => <li key={t.id}>{t.title}</li>)}</ul>
</>
)
}
When the mutation succeeds, invalidating ['todos'] triggers an automatic refetch, and every component reading that key updates. Query is at version 5 as of October 2026, with adapters for React, Vue, Solid, Svelte and Angular. The React package is @tanstack/react-query; if you see "React Query" in older material, it is the same library.
Two mistakes undo most of the benefit. The first is copying the response into useState, which cuts you off from automatic refetching; read from the query cache instead. The second is an incomplete query key. Include every variable the fetch depends on, as in ['todos', { page, filter }], or you will see stale data. And don't use Query as a client-state store for things like open modals. That belongs in React state, Zustand or TanStack Store.
Query vs SWR vs RTK Query
All three solve server-state caching, and the right choice depends more on your stack than on features.
Aspect | TanStack Query | SWR | RTK Query |
|---|
Maintainer | TanStack | Vercel | Redux team (part of Redux Toolkit) |
Frameworks | React, Vue, Solid, Svelte, Angular | React | Redux; React hooks provided |
API style | Hooks with query keys | Minimal hooks keyed by URL or key | Central API slice with endpoints, generated hooks |
Mutations and invalidation | useMutation, invalidateQueries, optimistic updates | mutate and useSWRMutation; simpler model | Tag-based invalidation (providesTags / invalidatesTags) |
Devtools | Official devtools | Community tools | Redux DevTools |
Pagination and infinite | useInfiniteQuery, placeholder data | useSWRInfinite | Supported via endpoint options |
I'd choose TanStack Query by default, for any framework and especially when caching gets complicated. SWR makes sense for small React or Next.js apps that want the smallest possible API. RTK Query is the right call when the app is already built on Redux Toolkit or you generate clients from OpenAPI, because it keeps server and client state in one store and one devtools view.
Query also works fine inside Next.js. Many Next.js apps use it for client-side caching, mutations and infinite scrolling, often prefetching on the server and hydrating on the client. For where server state sits next to client state more broadly, see C# Corner's guide to state management in React in 2026.
TanStack Router: typed URLs
TanStack Router is a client-side router for React and Solid with full type inference. Route paths, path params, search params and loader data are all typed, so a broken link or a wrong param is a compile error rather than a runtime bug.
The feature that sets it apart is treating search params as state. You validate the query string with a schema, and the router gives you typed values:
// src/routes/products.tsx
import { createFileRoute, Link } from '@tanstack/react-router'
import { z } from 'zod'
const searchSchema = z.object({
page: z.number().catch(1),
q: z.string().catch(''),
})
export const Route = createFileRoute('/products')({
validateSearch: searchSchema,
loaderDeps: ({ search }) => ({ page: search.page, q: search.q }),
loader: ({ deps }) => fetchProducts(deps.page, deps.q),
component: Products,
})
function Products() {
const { page } = Route.useSearch() // typed: number
const products = Route.useLoaderData() // typed from the loader
return (
<>
<ul>{products.map((p) => <li key={p.id}>{p.name}</li>)}</ul>
<Link to="/products" search={(prev) => ({ ...prev, page: page + 1 })}>
Next page
</Link>
</>
)
}
It also generates routes from files, supports nested layouts and route-level code splitting, preloads on hover, and integrates with TanStack Query for caching loader data. You don't need Router to use Query, though; Query works just as well with React Router, Next.js or no router at all.
TanStack Start: the full-stack option
TanStack Start is a full-stack framework for React and Solid, built entirely on TanStack Router. It adds full-document server-side rendering, streaming, server functions, server routes for APIs, middleware, and client and server builds, running on Vite or Rsbuild. TanStack declared Start 1.0 stable on October 13, 2026, which means the team now commits to its APIs. React Server Components are available as an optional integration that the docs still describe as experimental.
The piece you'll use most is the server function: code that runs only on the server but can be called from loaders, components or event handlers with full type safety.
// src/utils/users.functions.ts
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
import { findUserById } from './users.server' // server-only DB code
export const getUser = createServerFn({ method: 'GET' })
.validator(z.object({ id: z.string() }))
.handler(async ({ data }) => {
return findUserById(data.id)
})
// src/routes/users.$id.tsx
export const Route = createFileRoute('/users/$id')({
loader: ({ params }) => getUser({ data: { id: params.id } }),
component: UserPage,
})
Recent Start releases use .validator(). Older tutorials show .inputValidator(), which was deprecated and later removed, so fix any code you copy. Also remember that server functions are callable endpoints. Validate input and check authorization inside the handler, as you would for any API.
Start deploys to Node servers and to Vercel, Netlify, Cloudflare Workers, Railway and Render; C# Corner has a walkthrough on deploying a TanStack Start application.
Start or Next.js?
Aspect | TanStack Start | Next.js |
|---|
Mental model | Client-first app with SSR, loaders and server functions | Server-first; React Server Components by default in the App Router |
Routing | TanStack Router: typed params, typed search params, typed links | File-system App Router; typed routes are available but search params are untyped by default |
Server code | createServerFn (typed RPC), server routes, middleware | Server Components, Server Actions, Route Handlers, middleware |
React Server Components | Optional, documented as experimental | Core feature |
Frameworks | React and Solid | React |
Build tool | Vite or Rsbuild | Next.js compiler with Turbopack |
Hosting | Node, Vercel, Netlify, Cloudflare, Railway, Render and more | Vercel first-class; self-hosted Node and other platforms supported |
Maturity | 1.0 in October 2026; smaller ecosystem | Mature, very large ecosystem and hiring pool |
The deciding factor is what you're building. For an interactive, app-like product such as a dashboard, SaaS app or admin tool, where end-to-end type safety matters and you'd like to stay on Vite, Start is a strong choice. For a content-heavy site that benefits from Server Components and static generation, or a team that needs the largest ecosystem and hiring pool, Next.js is still the safer bet. Start is production-ready on paper, but expect fewer examples and integrations than you're used to.
Table, Form and Virtual for data-heavy screens
TanStack Table
TanStack Table gives you sorting, filtering, grouping, pagination, row selection, column pinning and resizing as pure logic, and you render the <table> or div grid yourself. Version 9, the latest major, makes features opt-in: you register only what a table uses, which keeps bundles small and types precise.
import {
tableFeatures,
rowSortingFeature,
createSortedRowModel,
sortFns,
useTable,
} from '@tanstack/react-table'
import type { ColumnDef } from '@tanstack/react-table'
type Person = { firstName: string; age: number }
const features = tableFeatures({
rowSortingFeature,
sortedRowModel: createSortedRowModel(),
sortFns,
})
const columns: Array<ColumnDef<typeof features, Person>> = [
{ accessorKey: 'firstName', header: 'First name' },
{ accessorKey: 'age', header: 'Age' },
]
export function PeopleTable({ data }: { data: Person[] }) {
const table = useTable({ features, columns, data })
return (
<table>
<thead>
{table.getHeaderGroups().map((hg) => (
<tr key={hg.id}>
{hg.headers.map((header) => (
<th key={header.id} onClick={header.column.getToggleSortingHandler()}>
<table.FlexRender header={header} />
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id}>
{row.getAllCells().map((cell) => (
<td key={cell.id}><table.FlexRender cell={cell} /></td>
))}
</tr>
))}
</tbody>
</table>
)
}
Code written for v8 uses useReactTable, getCoreRowModel() and flexRender instead, and the Table docs include a migration guide. Whichever version you're on, memoize data and columns or keep them in state or the query cache. Passing a new array on every render is the classic Table performance bug.
TanStack Form
TanStack Form manages field values, sync and async validation per field or form-wide, touched and dirty state, and submission. Field names are type-checked against your default values.
import { useForm } from '@tanstack/react-form'
export function SignupForm() {
const form = useForm({
defaultValues: { username: '', age: 0 },
onSubmit: async ({ value }) => {
await fetch('/api/signup', { method: 'POST', body: JSON.stringify(value) })
},
})
return (
<form onSubmit={(e) => { e.preventDefault(); form.handleSubmit() }}>
<form.Field
name="age"
validators={{
onChange: ({ value }) => (value >= 13 ? undefined : 'Must be 13 or older'),
}}
children={(field) => (
<>
<input
type="number"
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.valueAsNumber)}
/>
{!field.state.meta.isValid && <em>{field.state.meta.errors.join(', ')}</em>}
</>
)}
/>
<button type="submit">Sign up</button>
</form>
)
}
It accepts Standard Schema validators such as Zod and Valibot, and pairs naturally with Start server functions for server-side validation. A common modern stack is Start, Query, Form and Table together, with Zod schemas shared between the client and server functions.
TanStack Virtual
Rendering thousands of DOM nodes freezes the browser. TanStack Virtual renders only the items in view and positions them inside a container sized to the full list:
import { useRef } from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'
export function BigList({ items }: { items: string[] }) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 35,
})
return (
<div ref={parentRef} style={{ height: 400, overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map((v) => (
<div
key={v.key}
style={{ position: 'absolute', top: 0, width: '100%',
height: v.size, transform: `translateY(${v.start}px)` }}
>
{items[v.index]}
</div>
))}
</div>
</div>
)
}
It handles vertical, horizontal and grid layouts and dynamic row heights, and has a guide for reverse feeds such as chat and log views. Combine it with Table when a grid has more rows than the browser can comfortably render.
TanStack DB: when the cache isn't enough
The difference between DB and Query is worth understanding before you reach for DB. Query caches the result of each request by key. DB loads data from your API or sync engine into typed collections, and components run live queries against them, with joins, filters and ordering, whose results update automatically. Writes are optimistic: the UI changes immediately and reconciles with the server afterwards. That makes it a fit for instant, offline-friendly interfaces over synced data.
You can start with an existing REST API, because DB can use Query underneath to load data:
import { useLiveQuery, eq } from '@tanstack/react-db'
// todoCollection is defined once with queryCollectionOptions({ queryKey, queryFn, getKey, onUpdate, ... })
function OpenTodos() {
const { data: todos } = useLiveQuery({
query: (q) =>
q.from({ todo: todoCollection })
.where(({ todo }) => eq(todo.completed, false))
.orderBy(({ todo }) => todo.createdAt, 'desc'),
})
return <ul>{todos.map((t) => <li key={t.id}>{t.text}</li>)}</ul>
}
The libraries page still labels DB as beta, but the team announced 1.0 RC1 on October 29, 2026 and describes the API as stable. Setup APIs changed during the beta, so follow the current quick start rather than older blog posts. The same goes for Start and Table v9, which both changed APIs during 2025 and 2026: check the version selector in the docs before trusting a tutorial.
AI, Pacer and the newer libraries
TanStack AI provides provider-agnostic building blocks for AI features: a server-side chat() function, client hooks such as useChat, tool calling, and adapters for more than 20 model providers, including OpenAI-compatible APIs. Version 1.0 shipped on October 23, 2026 with stable core APIs, while its agent "harness" packages are still experimental. C# Corner shows how to connect an app to different AI models with TanStack AI.
Pacer, still in beta, controls when functions run: debounce, throttle, rate limit, queue and batch, with cancellation and pending state you can render. Its most common use is keeping a search box from firing a request on every keystroke:
import { useState } from 'react'
import { useDebouncedValue } from '@tanstack/react-pacer'
export function Search() {
const [query, setQuery] = useState('')
const [debouncedQuery] = useDebouncedValue(query, { wait: 500 })
// pass debouncedQuery to useQuery so you send one request, not one per keystroke
return <input value={query} onChange={(e) => setQuery(e.target.value)} />
}
The rest are worth knowing about but not yet worth building on for most teams. Store is the small reactive store that powers state in Table v9, Pacer and others; you can use it directly, but it is alpha. Hotkeys handles keyboard shortcuts with scopes, sequences and user rebinding. Charts reached 1.0 in October 2026 with a "grammar of graphics" approach. Redact swaps React and React DOM in the browser for a smaller runtime without changing app code, but it renders synchronously, so test interaction-heavy screens before adopting it. Devtools, CLI, Config and Intent are tooling: a shared devtools panel, project scaffolding, package publishing helpers, and a way to ship instructions for AI coding agents alongside npm packages.
If you're new to TanStack, add Query to an existing app first. It solves a problem almost every app has, it teaches you the TanStack style, and it tells you quickly whether Router or Start are worth a closer look.
Join the conversation! Your thoughts help the community grow.