TanStack 全家桶入门指南:在 React 18+ 和 Vue 3 项目中快速上手

by JeariCk 7 min read
tanstack: 一套为现代Web开发打造的开源工具集,提供类型安全、框架无关的前端开发库

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

tanstack: 一套为现代Web开发打造的开源工具集,提供类型安全、框架无关的前端开发库
tanstack: 一套为现代Web开发打造的开源工具集,提供类型安全、框架无关的前端开发库

TanStack 是什么?

TanStack 是 Tanner Linsley 搞的一系列开源前端工具库。如果你之前用过 React Query、React Table,那你已经接触过它们的前身——现在它们统一归到了 TanStack 品牌下,并且全面支持了 Vue、Solid、Svelte 这些框架。

这哥们早期做 React Table v7 的时候,估计也没想到后来会扩张成整一个生态。2022 年中推出了 v8,完全用 TypeScript 重写。到现在已经不是 React 专属了。

三个库我们今天主要讲:

原用名解决什么问题
TanStack QueryReact Query / Vue Query数据请求、缓存、状态同步
TanStack Router类型安全的路由(仅 React 和 Solid)
TanStack TableReact 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一句话
QueryQueryClientProvider + useQuery + useMutation代替所有 useEffect + fetch 的数据管理
RoutercreateRouter + RouterProvider + useParams类型安全路由,参数自动推断
TableuseReactTable + createColumnHelper无头表格,要什么样式自己写
Vue 3 项目
```bash

npm install @tanstack/vue-query @tanstack/vue-table

```
关键 API一句话
QueryVueQueryPlugin + useQuery + useMutation替代 Vuex/Pinia 管理异步数据
TableuseVueTable + createColumnHelper + <FlexRender>无头表格,Vue 3 原生集成
Router继续用 Vue Router,等适配器排上

它本质上就是选”解耦”——Query 拆开数据层和 UI 层,Table 让你完全控制渲染结构,Router 靠类型系统把路由状态管得死死的。三个工具共享一句哲学:”给你一个坚实的引擎,车身你自己造。”

*参考链接:*

– [TanStack Query 官方文档]

– [TanStack Router 官方文档]

– [TanStack Table 官方文档]

📖 推荐阅读

看看这些文章,你可能会感兴趣:

React Server Components 指南:原理、实战与框架对比

浅析:Next.js 15 为什么成为 Agentic AI 应用标准?

React Compiler 1.0 来了:以后useMemo 和 useCallback 可以删了?

React 19 与 Vue 3.6:两种不同的前端哲学

DigitalOcean Referral Badge

This site is powered by DigitalOcean — reliable cloud hosting for developers.

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注