TanStack 功能介绍与使用场景
TanStack 是一个专注于构建无头(Headless) 、类型安全 且框架无关 的工具库集合。它的前身是著名的 React Query 团队,现已发展成为一个支持 React, Vue, Solid, Svelte, Angular 等多前端框架的生态系统。
其核心理念是: "学会一次,到处复用" 。你只需要掌握一套 API 逻辑,就可以在任何支持的框架中使用,极大地降低了跨框架开发的学习成本和迁移成本。
核心功能模块与使用场景
| 模块 | 原名 | 核心功能 | 典型使用场景 |
|---|---|---|---|
| TanStack Query | React Query | 异步状态管理。自动处理数据获取、缓存、后台同步、加载/错误状态、重试机制、窗口聚焦重新获取等。 | 替代手写的 axios + useState + useEffect 组合;管理复杂的服务器状态;需要离线缓存或乐观更新的场景。 |
| TanStack Table | React Table | 无头表格逻辑。提供排序、过滤、分页、分组、虚拟滚动等核心逻辑,不渲染任何 HTML,完全由你控制 UI。 | 构建高度定制化的企业级数据表格;需要复杂交互(如多列排序、服务端分页)的后台管理系统。 |
| TanStack Router | - | 类型安全的路由。利用文件系统路由和强大的类型推导,实现参数、搜索查询的端到端类型安全。 | 对类型安全要求极高的中大型应用;需要预加载数据或权限控制的复杂路由场景。 |
| TanStack Virtual | React Virtual | 列表虚拟化。高效渲染超长列表或网格,只渲染可视区域内的 DOM 元素。 | 聊天历史记录、无限滚动的信息流、大型数据表格的行虚拟化。 |
| TanStack Form | - | 表单管理。提供高性能的表单状态管理,支持复杂的验证逻辑(常与 Zod 结合)。 | 复杂的动态表单、向导式表单、需要高性能验证的大型表单。 |
| TanStack Start | - | 全栈元框架。基于 Vite 和 TanStack Router,提供 SSR、流式传输、服务器函数等能力。 | 构建类似 Next.js/Nuxt 的全栈应用,但希望使用 TanStack 生态的统一体验。 |
完整使用示例:Vue 3 vs React
以下将以最常用的 TanStack Query (数据获取) 和 TanStack Table (表格展示) 为例,对比 Vue 3 和 React 的完整用法。你会发现逻辑几乎一模一样,只是语法糖不同。
1. TanStack Query: 数据获取与缓存
场景:从 API 获取用户列表,并支持删除用户(突变)。
🅰️ Vue 3 示例 (@tanstack/vue-query)
安装:
bash
npm install @tanstack/vue-query
1. 初始化 ( main.ts):
typescript
import { createApp } from 'vue'
import { VueQueryPlugin } from '@tanstack/vue-query'
import App from './App.vue'
const app = createApp(App)
app.use(VueQueryPlugin) // 注册插件
app.mount('#app')
2. 使用组件 ( UserList.vue):
vue
<script setup lang="ts">
import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
import axios from 'axios'
// --- 1. 查询数据 (GET) ---
const fetchUsers = async () => {
const { data } = await axios.get('https://jsonplaceholder.typicode.com/users')
return data
}
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['users'], // 唯一的缓存键
queryFn: fetchUsers,
staleTime: 1000 * 60 * 5, // 5分钟内数据视为新鲜,不重新请求
})
// --- 2. 修改数据 (DELETE) ---
const queryClient = useQueryClient()
const deleteMutation = useMutation({
mutationFn: (id: number) => axios.delete(`https://jsonplaceholder.typicode.com/users/${id}`),
onSuccess: () => {
// 成功后使 'users' 查询失效,触发自动重新获取
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
const handleDelete = (id: number) => {
if(confirm('确定删除?')) {
deleteMutation.mutate(id)
}
}
</script>
<template>
<div>
<h2>用户列表 (Vue 3)</h2>
<!-- 加载状态 -->
<div v-if="isLoading">加载中...</div>
<!-- 错误状态 -->
<div v-else-if="error">错误: {{ (error as Error).message }}</div>
<!-- 数据列表 -->
<ul v-else>
<li v-for="user in data" :key="user.id">
{{ user.name }}
<button @click="handleDelete(user.id)" :disabled="deleteMutation.isPending.value">
{{ deleteMutation.isPending.value ? '删除中...' : '删除' }}
</button>
</li>
</ul>
<button @click="refetch" style="margin-top: 20px;">手动刷新</button>
</div>
</template>
⚛️ React 示例 (@tanstack/react-query)
安装:
bash
npm install @tanstack/react-query
1. 初始化 ( main.tsx):
tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App'
const queryClient = new QueryClient() // 创建客户端实例
ReactDOM.createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}> {/* 包裹应用 */}
<App />
</QueryClientProvider>,
)
2. 使用组件 ( UserList.tsx):
tsx
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import axios from 'axios'
export default function UserList() {
const queryClient = useQueryClient()
// --- 1. 查询数据 (GET) ---
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['users'],
queryFn: async () => {
const { data } = await axios.get('https://jsonplaceholder.typicode.com/users')
return data
},
staleTime: 1000 * 60 * 5,
})
// --- 2. 修改数据 (DELETE) ---
const deleteMutation = useMutation({
mutationFn: (id: number) => axios.delete(`https://jsonplaceholder.typicode.com/users/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
if (isLoading) return <div>加载中...</div>
if (error) return <div>错误: {(error as Error).message}</div>
return (
<div>
<h2>用户列表 (React)</h2>
<ul>
{data?.map((user: any) => (
<li key={user.id}>
{user.name}{' '}
<button
onClick={() => handleDelete(user.id)}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? '删除中...' : '删除'}
</button>
</li>
))}
</ul>
<button onClick={() => refetch()} style={{ marginTop: '20px' }}>手动刷新</button>
</div>
)
function handleDelete(id: number) {
if (confirm('确定删除?')) {
deleteMutation.mutate(id)
}
}
}
对比总结 : 逻辑配置(queryKey, queryFn, invalidateQueries)完全一致。Vue 中使用 .value 访问响应式对象(如 isPending.value),而 React 直接访问属性。
2. TanStack Table: 高级表格
场景 :展示用户数据,支持排序 和分页 。TanStack Table 是 "Headless" 的,意味着它只提供逻辑(行、列、页码计算),你需要自己写 <table> 标签。
🅰️ Vue 3 示例 (@tanstack/vue-table)
安装:
bash
npm install @tanstack/vue-table
组件代码 ( UserTable.vue):
vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import {
FlexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from '@tanstack/vue-table'
// 模拟数据
const data = ref([
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 },
{ id: 4, name: 'David', age: 20 },
])
// 定义列
const columns = [
{
header: '姓名',
accessorKey: 'name',
cell: ({ getValue }: any) => getValue<string>(),
},
{
header: '年龄',
accessorKey: 'age',
cell: ({ getValue }: any) => getValue<number>(),
},
]
// 初始化表格
const table = useVueTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
state: {
// 可以在此绑定响应式状态
},
})
</script>
<template>
<div>
<table border="1" cellpadding="8">
<thead>
<tr v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
<th
v-for="header in headerGroup.headers"
:key="header.id"
@click="header.column.toggleSorting()"
style="cursor: pointer; user-select: none;"
>
<FlexRender :render="header.column.columnDef.header" :props="header.getContext()" />
{{ header.column.getIsSorted() ? (header.column.getIsSorted() === 'asc' ? ' 🔼' : ' 🔽') : '' }}
</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>
<!-- 分页控件 -->
<div style="margin-top: 10px;">
<button
@click="table.setPageIndex(0)"
:disabled="!table.getCanPreviousPage()"
>
首页
</button>
<button
@click="table.previousPage()"
:disabled="!table.getCanPreviousPage()"
>
上一页
</button>
<span> 第 {{ table.getState().pagination.pageIndex + 1 }} 页 </span>
<button
@click="table.nextPage()"
:disabled="!table.getCanNextPage()"
>
下一页
</button>
<button
@click="table.setPageIndex(table.getPageCount() - 1)"
:disabled="!table.getCanNextPage()"
>
末页
</button>
</div>
</div>
</template>
⚛️ React 示例 (@tanstack/react-table)
安装:
bash
npm install @tanstack/react-table
组件代码 ( UserTable.tsx):
tsx
import { useState } from 'react'
import {
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
ColumnDef,
} from '@tanstack/react-table'
type Person = { id: number; name: string; age: number }
const data: Person[] = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 },
{ id: 3, name: 'Charlie', age: 35 },
{ id: 4, name: 'David', age: 20 },
]
const columns: ColumnDef<Person>[] = [
{
header: '姓名',
accessorKey: 'name',
cell: ({ getValue }) => getValue<string>(),
},
{
header: '年龄',
accessorKey: 'age',
cell: ({ getValue }) => getValue<number>(),
},
]
export default function UserTable() {
const [sorting, setSorting] = useState([])
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 2 })
const table = useReactTable({
data,
columns,
state: { sorting, pagination },
onSortingChange: setSorting,
onPaginationChange: setPagination,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
})
return (
<div>
<table border="1" cellPadding="8">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
onClick={header.column.getToggleSortingHandler()}
style={{ cursor: 'pointer', userSelect: 'none' }}
>
{flexRender(header.column.columnDef.header, header.getContext())}
{{ asc: ' 🔼', desc: ' 🔽' }[header.column.getIsSorted() as string] ?? null}
</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>
{/* 分页控件 */}
<div style={{ marginTop: '10px' }}>
<button onClick={() => table.setPageIndex(0)} disabled={!table.getCanPreviousPage()}>
首页
</button>
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
上一页
</button>
<span> 第 {table.getState().pagination.pageIndex + 1} 页 </span>
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
下一页
</button>
<button
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
末页
</button>
</div>
</div>
)
}
对比总结:
总结
TanStack 的最大优势在于统一性 。一旦你理解了 queryKey 的概念或 useTable 的配置项,你就可以在 React、Vue 甚至 Solid 项目中无缝切换,无需重新学习新的 API。对于现代前端工程化而言,它是提升开发效率和代码质量的神器。