跳到主要内容

Query 与生态

很多人第一次对 TanStack 产生好感,其实不是从 Start 开始,而是从 Query 开始。

这很正常。TanStack Query 太常用了。它几乎已经成了“前端异步状态管理应该怎么做”的一条事实标准。

但如果你只把 TanStack 理解成 Query,那会少看掉后面一整条很顺的技术线。

1. Query 在整套体系里是什么位置

它解决的是:服务端状态怎么在前端应用里稳定地活着。

更具体一点:

  • 请求之后怎么缓存
  • 数据什么时候算旧
  • 什么时候该重取
  • 列表和详情怎么共享结果
  • 写操作之后怎么失效

这一层和 fetchaxios 不是同一个问题,也和“我用不用 SSR 框架”不是同一个问题。

一个最小 QueryClient 配置

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
gcTime: 5 * 60_000,
retry: 2,
refetchOnWindowFocus: false,
},
mutations: {
retry: 1,
},
},
})

export function App() {
return (
<QueryClientProvider client={queryClient}>
<RouterApp />
</QueryClientProvider>
)
}

这类默认值很适合大多数业务后台。它不会神奇,但能让 Query 的行为先稳定下来。

一个最常见的查询写法

import { useQuery } from '@tanstack/react-query'

async function fetchUsers() {
const res = await fetch('/api/users')

if (!res.ok) {
throw new Error('用户列表获取失败')
}

return res.json() as Promise<Array<{ id: string; name: string }>>
}

export function UserList() {
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
})

if (query.isPending) return <p>加载中...</p>
if (query.isError) return <p>{query.error.message}</p>

return (
<ul>
{query.data.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}

如果你想补更完整的 Query 内容,这个站点里已经有一组现成专题:

2. Query 和 Router 怎么配

这两个库一起用时,体验通常很好,因为职责划分比较顺:

  • Router 管页面切换、URL、Loader、预加载
  • Query 管异步状态、缓存生命周期、更新与失效

一个更常见的组合模式

import { queryOptions } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'

const usersQuery = queryOptions({
queryKey: ['users'],
queryFn: fetchUsers,
})

export const Route = createFileRoute('/users')({
loader: async ({ context }) => {
await context.queryClient.ensureQueryData(usersQuery)
},
component: UsersPage,
})

这样做的好处是:

  • 首次进入页面时,路由层先把数据准备好
  • 组件内如果继续用同一个 queryKey,会直接命中缓存

3. Query 和 Start 怎么配

Start 本身已经有服务端执行和路由级数据能力,所以很多人会问:那我还要不要 Query?

我的建议通常是:

继续要 Query 的场景

  • 数据会跨多个组件、多个页面复用
  • mutation 很多,失效逻辑复杂
  • 你已经有成熟的 Query 心智和工具链

可以先少用 Query 的场景

  • 页面数据主要靠路由级 loader 就能收住
  • 应用规模还不大
  • 你想先把 Start 主线跑顺,再慢慢补异步状态层

一个 Start 里常见的 Provider 接法

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { Outlet, createRootRoute } from '@tanstack/react-router'

const queryClient = new QueryClient()

export const Route = createRootRoute({
component: RootLayout,
})

function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
<Outlet />
</QueryClientProvider>
)
}

再看一个 mutation 例子

import { useMutation, useQueryClient } from '@tanstack/react-query'

async function createUser(input: { name: string }) {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
})

if (!res.ok) {
throw new Error('创建失败')
}

return res.json()
}

export function CreateUserButton() {
const queryClient = useQueryClient()

const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})

return (
<button onClick={() => mutation.mutate({ name: 'Tanner' })}>
新建用户
</button>
)
}

4. Form、Table、Virtual 怎么看

这三块不是“可有可无的小配件”,而是很多中后台项目迟早会碰到的真实需求。

TanStack Form

适合:

  • 表单联动多
  • 校验逻辑复杂
  • 需要数组字段、异步校验、组合表单
  • 不想被某个 UI 框架的表单组件绑住

一个最小表单例子

import { useForm } from '@tanstack/react-form'

export function LoginForm() {
const form = useForm({
defaultValues: {
email: '',
password: '',
},
onSubmit: async ({ value }) => {
console.log(value)
},
})

return (
<form
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<input
value={form.state.values.email}
onChange={(e) => form.setFieldValue('email', e.target.value)}
/>
<input
type="password"
value={form.state.values.password}
onChange={(e) => form.setFieldValue('password', e.target.value)}
/>
<button type="submit">登录</button>
</form>
)
}

如果你已经站在 Zod + TypeScript + TanStack 这条线,Form 往往会很顺。

TanStack Table

它是典型的“逻辑很强,UI 你自己做”的 headless 表格库。

适合:

  • 后台表格很多
  • 排序、筛选、分页、分组、列配置都比较重
  • 现成 UI 表格已经不够灵活

一个最小表格例子

import {
createColumnHelper,
flexRender,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table'

type User = { id: string; name: string; email: string }

const columnHelper = createColumnHelper<User>()

const columns = [
columnHelper.accessor('name', {
header: '姓名',
cell: (info) => info.getValue(),
}),
columnHelper.accessor('email', {
header: '邮箱',
cell: (info) => info.getValue(),
}),
]

export function UserTable({ data }: { data: User[] }) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
})

return (
<table>
<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>
)
}

尤其和 Router + Query 放一起时,你会很容易把表格数据、URL 状态和缓存失效整成一套结构。

TanStack Virtual

这个很容易被低估。表格、大列表、聊天流、时间轴,只要渲染量一上去,它就会立刻变得有存在感。

一个最小虚拟列表示例

import { useRef } from 'react'
import { useVirtualizer } from '@tanstack/react-virtual'

export function MessageList({ items }: { items: string[] }) {
const parentRef = useRef<HTMLDivElement | null>(null)

const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 44,
overscan: 8,
})

return (
<div ref={parentRef} style={{ height: 320, overflow: 'auto' }}>
<div
style={{
height: rowVirtualizer.getTotalSize(),
position: 'relative',
}}
>
{rowVirtualizer.getVirtualItems().map((item) => (
<div
key={item.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: item.size,
transform: `translateY(${item.start}px)`,
}}
>
{items[item.index]}
</div>
))}
</div>
</div>
)
}

如果你已经在用 Table,那 Virtual 基本属于迟早会碰到的邻居。

5. 一套比较顺的 TanStack 组合长什么样

轻量应用

  • TanStack Query

就够了。别为了“体系统一”强行上全家桶。

中型后台

  • TanStack Router
  • TanStack Query
  • TanStack Table
  • TanStack Virtual

这套组合很常见,也很能打。

React 全栈应用

  • TanStack Start
  • TanStack Router
  • TanStack Query
  • 按需再补 Form / Table / Virtual

这时整套技术线会很顺,类型和工程边界也比较统一。

6. 一个更实际的建议

如果你现在只打算认真补一块,我会建议按这个优先级看:

  1. Query
  2. Router
  3. Start
  4. Form / Table / Virtual

原因不复杂:前两者最容易先在项目里带来直接收益,Start 更像架构层决定,适合你确认方向之后再上。

小结

TanStack 最强的地方,从来不是某一个单点库特别炫,而是这些工具放在一起之后,边界很顺:

  • 路由一套逻辑
  • 异步状态一套逻辑
  • 服务端调用一套逻辑
  • 表单、表格、大列表也都能接进来

这也是为什么很多团队先从 Query 入门,最后会一路走到 Router,甚至走到 Start。