TanStack 是一套”无头”(headless)工具库,覆盖查询、路由、表格三大场景。在 React 18+ 和 Vue 3 项目中,用几行代码就能搞定数据请求、页面导航和数据表格——而且全都类型安全。

TanStack 是什么?
TanStack 是 Tanner Linsley 搞的一系列开源前端工具库。如果你之前用过 React Query、React Table,那你已经接触过它们的前身——现在它们统一归到了 TanStack 品牌下,并且全面支持了 Vue、Solid、Svelte 这些框架。
这哥们早期做 React Table v7 的时候,估计也没想到后来会扩张成整一个生态。2022 年中推出了 v8,完全用 TypeScript 重写。到现在已经不是 React 专属了。
三个库我们今天主要讲:
| 库 | 原用名 | 解决什么问题 |
|---|---|---|
| TanStack Query | React Query / Vue Query | 数据请求、缓存、状态同步 |
| TanStack Router | — | 类型安全的路由(仅 React 和 Solid) |
| TanStack Table | React Table | 无头表格引擎,多框架覆盖 |
为什么选它们? 数据请求这块,很多框架本身没给完整的解决方案。大家要么自己写一堆 useEffect + fetch 的模板代码,要么把 Redux/Pinia 当缓存用。Query 就是来干掉这些重复劳动的。Router 和 Table 同理——都是”给你引擎,车身自己搭”的路线。
开始前的准备
React 18+ 项目
```bash
# 已有项目
npm install @tanstack/react-query @tanstack/react-router @tanstack/react-table
# 从头起新项目
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install @tanstack/react-query @tanstack/react-router @tanstack/react-table
```
要求:React 18+(支持 createRoot),TypeScript 5.3+(推荐但不是必须)。
Vue 3 项目
```bash
# 已有项目
npm install @tanstack/vue-query @tanstack/vue-table
# 新项目
npm create vite@latest my-app -- --template vue-ts
cd my-app
npm install @tanstack/vue-query @tanstack/vue-table
```
注意:Router 目前只支持 React 和 Solid,Vue 还没排上。
TanStack Query:你的数据请求救星
React 18+ 中使用
先得有个 QueryClient 包住整个应用。这步有点像 Redux 的 Provider,但只管数据请求。
```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 默认配置已经挺聪明了:重复请求自动去重、数据缓存、后台刷新、窗口聚焦时重新拉取……开箱即用。
写个组件用 `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>加载中...</div>
if (error) return <div>出错了:{error.message}</div>
return (
<ul>
{data?.map(user => (
<li key={user.id}>{user.name} — {user.email}</li>
))}
</ul>
)
}
```
核心概念就三个:
1. Queries—— `useQuery` 拉数据,自带缓存
2. Mutations—— `useMutation` 增删改
3. Query Invalidation—— 改完数据后让查询重新拉取
串起来看一个:
```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>加载中...</div>
return (
<div>
<button onClick={() => mutation.mutate('买酱油')}>
{mutation.isPending ? '添加中...' : '添加 Todo'}
</button>
<ul>
{todos?.map(todo => (
<li key={todo.id}>
{todo.title} {todo.completed ? '✅' : '⬜'}
</li>
))}
</ul>
</div>
)
}
```
注意几点:
– `queryKey: [‘todos’]` 是缓存的唯一标识
– `useQuery` 自动处理了 loading、error、data 三态
– `useMutation` + `invalidateQueries` 就是”改数据 → 自动刷新”的标准模式
– 你不再需要手动写 `useEffect` + `useState` 了
以前写一个页面可能十几行甚至几十行数据管理代码,现在几行完事。
Vue 3 中使用
Vue 版差不多,只是把 hooks 换成了 composables:
```bash
npm install @tanstack/vue-query
```
入口注入:
```ts
// main.ts
import { createApp } from 'vue'
import { VueQueryPlugin } from '@tanstack/vue-query'
import App from './App.vue'
createApp(App).use(VueQueryPlugin)
```
组件里直接用:
```vue
<!-- components/UserList.vue -->
<template>
<div v-if="isLoading">加载中...</div>
<div v-else-if="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 用 `useMutation`:
```vue
<!-- components/TodoApp.vue -->
<template>
<button @click="handleAdd" :disabled="mutation.isPending.value">
{{ mutation.isPending.value ? '添加中...' : '添加 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('买酱油')
}
</script>
```
说句实话,Vue Query 的 API 和 React Query 高度一致,从 React 转到 Vue 基本不用换脑子。唯一区别是 Vue 的响应式系统会自己追踪 data 变化,模板直接绑就行。
TanStack Router:类型安全的现代路由方案
路由这块,Router 目前只支持 React 和 Solid。Vue 适配器还排着队呢,所以 Vue 项目继续用 Vue Router。
它最大的卖点是类型安全——路由参数、search params、state 全部有类型推断。不像 react-router-dom 那样 `useParams()` 拿到的全是 `string | undefined`,你还得自己再断言一遍。
安装:
```bash
npm install @tanstack/react-router
```
支持文件路由和代码路由两种方式。先看代码路由快速上手:
```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
```
入口使用:
```tsx
// main.tsx
import { RouterProvider } from '@tanstack/react-router'
import router from './router'
createRoot(document.getElementById('root')!).render(
<RouterProvider router={router} />
)
```
在 `UserDetail` 里,参数类型自动推断:
```tsx
// pages/UserDetail.tsx
import { useParams } from '@tanstack/react-router'
export default function UserDetail() {
const { userId } = useParams({ from: '/users/$userId' })
return <div>用户 ID: {userId}</div>
}
```
和 react-router-dom 比呢?最直接的区别就是调用 useParams 不需要再手动声明参数类型。但凡你维护过超过三层嵌套的路由就知道,每次改路由参数然后补类型声明有多烦。
search params 也是类型安全的:
```tsx
import { useSearch } from '@tanstack/react-router'
const search = useSearch({ from: '/users' })
// search.page 的类型已经在路由定义时指定了
```
TanStack Table:万能数据表格引擎
Table 走的是”无头”路线。不给你任何预定义的样式或标签结构,给你一套 hooks/composables,你自己控制 HTML 和 CSS。这意味着你可以做出任何样式的表格——从最简单的列表到企业级数据网格。
React 18+ 中使用
```bash
npm install @tanstack/react-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: '张三', age: 28, status: 'Active' },
{ name: '李四', age: 35, status: 'Inactive' },
{ name: '王五', age: 42, status: 'Pending' },
]
const columnHelper = createColumnHelper<Person>()
const columns = [
columnHelper.accessor('name', {
header: '姓名',
cell: info => info.getValue(),
}),
columnHelper.accessor('age', {
header: '年龄',
cell: info => info.getValue(),
}),
columnHelper.accessor('status', {
header: '状态',
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>
)
}
```
加上排序和分页也不复杂:
```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>{/* 同上面渲染方式 */}</table>
<div>
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
上一页
</button>
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
下一页
</button>
</div>
</div>
)
}
```
这里有个隐藏福利:`useReactTable` 的 state 是完全可控的。你可以把 table state 放到 Redux/Zustand 或者 URL search params 里,实现”刷新页面后恢复表格状态”这种操作。
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: '张三', age: 28, status: 'Active' },
{ name: '李四', age: 35, status: 'Inactive' },
{ name: '王五', age: 42, status: 'Pending' },
]
const columnHelper = createColumnHelper<Person>()
const columns = [
columnHelper.accessor('name', { header: '姓名' }),
columnHelper.accessor('age', { header: '年龄' }),
columnHelper.accessor('status', { header: '状态' }),
]
const table = useVueTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})
</script>
```
注意 React 里是 `useReactTable` + `flexRender()` 函数,Vue 里是 `useVueTable` + `<FlexRender>` 组件——适配器层面的小差异,逻辑完全一样。
实战小结
React 18+ 项目
```bash
npm install @tanstack/react-query @tanstack/react-router @tanstack/react-table
```
| 库 | 关键 API | 一句话 |
|---|---|---|
| Query | QueryClientProvider + useQuery + useMutation | 代替所有 useEffect + fetch 的数据管理 |
| Router | createRouter + RouterProvider + useParams | 类型安全路由,参数自动推断 |
| Table | useReactTable + createColumnHelper | 无头表格,要什么样式自己写 |
Vue 3 项目
```bash
npm install @tanstack/vue-query @tanstack/vue-table
```
| 库 | 关键 API | 一句话 |
|---|---|---|
| Query | VueQueryPlugin + useQuery + useMutation | 替代 Vuex/Pinia 管理异步数据 |
| Table | useVueTable + createColumnHelper + <FlexRender> | 无头表格,Vue 3 原生集成 |
| Router | — | 继续用 Vue Router,等适配器排上 |
—
它本质上就是选”解耦”——Query 拆开数据层和 UI 层,Table 让你完全控制渲染结构,Router 靠类型系统把路由状态管得死死的。三个工具共享一句哲学:”给你一个坚实的引擎,车身你自己造。”
*参考链接:*
📖 推荐阅读
看看这些文章,你可能会感兴趣:
React Server Components 指南:原理、实战与框架对比
浅析:Next.js 15 为什么成为 Agentic AI 应用标准?