TanStack is a set of headless utility libraries covering data fetching, routing, and tables. In React 18+ and Vue 3 projects, a few lines of code gets you data requests, page navigation, and data tables — all type-safe.

What is TanStack?
TanStack is an open-source collection of frontend tools created by Tanner Linsley. If you’ve used React Query or React Table before, you’ve already met their predecessors — they’ve now been rebranded under TanStack and support Vue, Solid, Svelte, and more.
Back when he was building React Table v7, I doubt he expected it to grow into a whole ecosystem. V8 dropped in mid-2022 with a full TypeScript rewrite and support beyond React. By 2025-2026, people are comparing TanStack Start (their full-stack React framework) with Next.js and Remix.
Three libraries we’re covering today:
| Library | Former Name | What It Solves |
|---|---|---|
| TanStack Query | React Query / Vue Query | Data fetching, caching, state sync |
| TanStack Router | — | Type-safe routing (React & Solid only) |
| TanStack Table | React Table | Headless table engine, multi-framework |
Why these? Most frameworks don’t ship a complete data-fetching solution. Developers end up writing mountains of `useEffect` + `fetch` boilerplate, or misusing Redux/Pinia as a cache. TanStack Query kills all that repetition. Router and Table follow the same philosophy — “here’s your engine, you build the body.”
Prerequisites
React 18+ Project
```bash
# Existing project
npm install @tanstack/react-query @tanstack/react-router @tanstack/react-table
# New project with Vite
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install @tanstack/react-query @tanstack/react-router @tanstack/react-table
```
Requirements: React 18+ (with createRoot), TypeScript 5.3+ (recommended but not required).
Vue 3 Project
```bash
# Existing project
npm install @tanstack/vue-query @tanstack/vue-table
# New project
npm create vite@latest my-app -- --template vue-ts
cd my-app
npm install @tanstack/vue-query @tanstack/vue-table
```
Note: Router only supports React and Solid for now — Vue is still waiting in line. Vue projects should stick with Vue Router for navigation, but Query and Table work just as well.
TanStack Query: Your Data Fetching Savior
In React 18+
You need a `QueryClient` wrapping your whole app. Think of it like Redux’s Provider, but for data fetching only.
```tsx
// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createRoot } from 'react-dom/client'
import App from './App'
const queryClient = new QueryClient()
createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
)
```
QueryClient’s defaults are already pretty smart: automatic request deduplication, caching, background refetch, refetch on window focus — all works out of the box.
A component with `useQuery`:
```tsx
// components/UserList.tsx
import { useQuery } from '@tanstack/react-query'
type User = {
id: number
name: string
email: string
}
const fetchUsers = async (): Promise<User[]> => {
const res = await fetch('https://jsonplaceholder.typicode.com/users')
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
}
export default function UserList() {
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data?.map(user => (
<li key={user.id}>{user.name} — {user.email}</li>
))}
</ul>
)
}
```
Three core concepts:
1. Queries — `useQuery` fetches data with built-in caching
2. Mutations — `useMutation` creates, updates, or deletes data
3. Query Invalidation — tells queries to re-fetch after data changes
Putting it together:
```tsx
// components/TodoApp.tsx
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
type Todo = { id: number; title: string; completed: boolean }
const fetchTodos = async (): Promise<Todo[]> => {
const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5')
return res.json()
}
const addTodo = async (title: string) => {
const res = await fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST',
body: JSON.stringify({ title, completed: false }),
headers: { 'Content-Type': 'application/json' },
})
return res.json()
}
export default function TodoApp() {
const queryClient = useQueryClient()
const { data: todos, isLoading } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
const mutation = useMutation({
mutationFn: (title: string) => addTodo(title),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
if (isLoading) return <div>Loading...</div>
return (
<div>
<button onClick={() => mutation.mutate('Buy groceries')}>
{mutation.isPending ? 'Adding...' : 'Add Todo'}
</button>
<ul>
{todos?.map(todo => (
<li key={todo.id}>
{todo.title} {todo.completed ? '✅' : '⬜'}
</li>
))}
</ul>
</div>
)
}
```
Key takeaways:
– `queryKey: [‘todos’]` is the cache identifier, globally unique
– `useQuery` handles loading, error, and data states automatically
– `useMutation` + `invalidateQueries` is the standard “mutate → refresh” pattern
– No more manual `useEffect` + `useState`
A page that used to require 30 lines of data-fetching boilerplate now takes just a few.
In Vue 3
Same idea, hooks become composables:
```bash
npm install @tanstack/vue-query
```
Plugin setup:
```ts
// main.ts
import { createApp } from 'vue'
import { VueQueryPlugin } from '@tanstack/vue-query'
import App from './App.vue'
createApp(App).use(VueQueryPlugin)
```
Use in a component:
```vue
<!-- components/UserList.vue -->
<template>
<div v-if="isLoading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ul v-else>
<li v-for="user in data" :key="user.id">
{{ user.name }} — {{ user.email }}
</li>
</ul>
</template>
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
type User = { id: number; name: string; email: string }
const fetchUsers = async (): Promise<User[]> => {
const res = await fetch('https://jsonplaceholder.typicode.com/users')
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
}
const { data, isLoading, error } = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})
</script>
```
Mutation with `useMutation`:
```vue
<!-- components/TodoApp.vue -->
<template>
<button @click="handleAdd" :disabled="mutation.isPending.value">
{{ mutation.isPending.value ? 'Adding...' : 'Add Todo' }}
</button>
<ul>
<li v-for="todo in data" :key="todo.id">
{{ todo.title }} {{ todo.completed ? '✅' : '⬜' }}
</li>
</ul>
</template>
<script setup lang="ts">
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
const queryClient = useQueryClient()
type Todo = { id: number; title: string; completed: boolean }
const { data } = useQuery({
queryKey: ['todos'],
queryFn: async (): Promise<Todo[]> => {
const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5')
return res.json()
},
})
const mutation = useMutation({
mutationFn: (title: string) =>
fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST',
body: JSON.stringify({ title, completed: false }),
headers: { 'Content-Type': 'application/json' },
}).then(r => r.json()),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
const handleAdd = () => {
mutation.mutate('Buy groceries')
}
</script>
```
Honestly, Vue Query’s API is nearly identical to React Query’s. Switching from React to Vue requires zero mental overhead. The only difference — Vue’s reactivity system automatically tracks data changes, so you just bind directly in the template.
TanStack Router: Type-Safe Modern Routing
Router only supports React and Solid for now. Vue adapters are still in the queue, so stick with Vue Router for your Vue projects.
Its biggest selling point is type safety — route params, search params, and state all have type inference built in. No more `useParams()` returning `string | undefined` like react-router-dom, forcing you to assert types manually.
Install:
```bash
npm install @tanstack/react-router
```
Supports both file-based and code-based routing. Let’s start with code-based:
```tsx
// src/router.tsx
import { createRouter, createRoute, createRootRoute } from '@tanstack/react-router'
import App from './App'
import Home from './pages/Home'
import UserDetail from './pages/UserDetail'
const rootRoute = createRootRoute({
component: App,
})
const homeRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: Home,
})
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
component: UserDetail,
})
const routeTree = rootRoute.addChildren([homeRoute, userRoute])
const router = createRouter({ routeTree })
export default router
```
Usage in entry:
```tsx
// main.tsx
import { RouterProvider } from '@tanstack/react-router'
import router from './router'
createRoot(document.getElementById('root')!).render(
<RouterProvider router={router} />
)
```
In `UserDetail`, params are auto-inferred:
```tsx
// pages/UserDetail.tsx
import { useParams } from '@tanstack/react-router'
export default function UserDetail() {
const { userId } = useParams({ from: '/users/$userId' })
return <div>User ID: {userId}</div>
}
```
What’s the difference from react-router-dom? You don’t manually declare param types. If you’ve ever maintained routing with more than three levels of nesting, you know the pain of changing route params and chasing type declarations everywhere.
Search params are also type-safe:
```tsx
import { useSearch } from '@tanstack/react-router'
const search = useSearch({ from: '/users' })
// search.page type is already defined at route definition
```
TanStack Table: Universal Data Table Engine
TanStack Table goes the “headless” route. No pre-built styles or tag structures — it gives you hooks/composables, and you control the HTML and CSS. That means you can build anything from a simple list to an enterprise data grid.
In React 18+
```bash
npm install @tanstack/react-table
```
Create a simple table:
```tsx
// components/BasicTable.tsx
import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table'
type Person = {
name: string
age: number
status: 'Active' | 'Inactive' | 'Pending'
}
const data: Person[] = [
{ name: 'Alice', age: 28, status: 'Active' },
{ name: 'Bob', age: 35, status: 'Inactive' },
{ name: 'Charlie', age: 42, status: 'Pending' },
]
const columnHelper = createColumnHelper<Person>()
const columns = [
columnHelper.accessor('name', {
header: 'Name',
cell: info => info.getValue(),
}),
columnHelper.accessor('age', {
header: 'Age',
cell: info => info.getValue(),
}),
columnHelper.accessor('status', {
header: 'Status',
cell: info => {
const status = info.getValue()
const color = status === 'Active' ? 'green' : status === 'Inactive' ? 'gray' : 'orange'
return <span style={{ color }}>{status}</span>
},
}),
]
export default function BasicTable() {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<table border={1}>
<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>
)
}
```
Adding sorting and pagination:
```tsx
import {
getSortedRowModel,
getPaginationRowModel,
} from '@tanstack/react-table'
import { useState } from 'react'
export default function SortableTable() {
const [sorting, setSorting] = useState([])
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
state: { sorting },
onSortingChange: setSorting,
})
return (
<div>
<table>{/* same rendering as above */}</table>
<div>
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
Previous
</button>
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
Next
</button>
</div>
</div>
)
}
```
Hidden bonus: `useReactTable`’s `state` is fully controllable. You can put table state into Redux/Zustand or URL search params for “restore table state on page refresh.”
In Vue 3
```bash
npm install @tanstack/vue-table
```
```vue
<!-- components/BasicTable.vue -->
<template>
<table border="1">
<thead>
<tr v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<th v-for="header in headerGroup.headers" :key="header.id">
<FlexRender :render="header.column.columnDef.header" :props="header.getContext()" />
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in table.getRowModel().rows" :key="row.id">
<td v-for="cell in row.getVisibleCells()" :key="cell.id">
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
import {
createColumnHelper,
getCoreRowModel,
useVueTable,
FlexRender,
} from '@tanstack/vue-table'
type Person = { name: string; age: number; status: string }
const data: Person[] = [
{ name: 'Alice', age: 28, status: 'Active' },
{ name: 'Bob', age: 35, status: 'Inactive' },
{ name: 'Charlie', age: 42, status: 'Pending' },
]
const columnHelper = createColumnHelper<Person>()
const columns = [
columnHelper.accessor('name', { header: 'Name' }),
columnHelper.accessor('age', { header: 'Age' }),
columnHelper.accessor('status', { header: 'Status' }),
]
const table = useVueTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
</script>
```
React uses `useReactTable` + `flexRender()` function; Vue uses `useVueTable` + `<FlexRender>` component. Small adapter-level difference, same logic underneath.
Quick Reference
React 18+ Project
```bash
npm install @tanstack/react-query @tanstack/react-router @tanstack/react-table
```
| Library | Key APIs | One-liner |
|---|---|---|
| Query | QueryClientProvider + useQuery + useMutation | Replaces all useEffect + fetch data management |
| Router | createRouter + RouterProvider + useParams | Type-safe routing, auto-inferred params |
| Table | useReactTable + createColumnHelper | Headless table, any markup you want |
Vue 3 Project
```bash
npm install @tanstack/vue-query @tanstack/vue-table
```
| Library | Key APIs | One-liner |
|---|---|---|
| Query | VueQueryPlugin + useQuery + useMutation | Replaces Pinia for async data management |
| Table | useVueTable + createColumnHelper + <FlexRender> | Headless table, native Vue 3 integration |
| Router | — | Stick with Vue Router for now |
—
Choosing TanStack means choosing decoupling — Query separates your data layer from UI, Table gives you full control over rendering, and Router locks down routing state through the type system. Three tools, one shared philosophy: “Here’s your engine — you build the body yourself.”
In a frontend ecosystem that keeps reinventing itself every 18 months, this toolkit might be one of the few bets worth placing long-term.
*References:*
– [TanStack Query Official Docs]
– [TanStack Router Official Docs]
– [TanStack Table Official Docs]
📖 Recommended Reading
Take a look at these articles, you may be interested
RSC(React Server Components) Guide — How They Work, Code Examples, and Framework Comparison
Introductory Analysis: Why Next.js 15 Became the Standard for Agentic AI Apps
Web Components + Astro + htmx: The 2026 Lightweight Frontend Trio
React Compiler 1.0 Is Here: Can We Finally Delete useMemo and useCallback?