./read "TanStack Start 实战指南:..."

TanStack Start 实战指南:Start + Router + Query + Store 全链路

一步步动手体验 TanStack Start 生态,利用 Router、Query、Store 模块搭建可运行的知识库应用

tanstack_start_实战指南:start_+_ro.md2025-11-21
./meta --show-details
Published
2025年11月21日
Reading
8 min
Words
7,700
Status
PUBLISHED

TanStack Start 实战指南:Start + Router + Query + Store 全链路

TanStack 团队在 2024 年发布了 TanStack Start,一个结合 Router、Query、Virtual、Form、Store 等模块的 React 全栈脚手架。本文按照实际开发顺序,带你用 Start、Router、Query、Store 实现一个「知识库」小应用,涵盖工程化、数据获取、状态管理和生产部署要点。

1. TanStack 生态总览

| 模块 | 作用 | 在本示例中的职责 | | --- | --- | --- | | Start | 提供 Vite + React + TanStack Router 组合的 SSR/SSG 能力,支持服务器 Actions | 脚手架、构建与部署 | | Router | 文件式/声明式路由、数据加载器、错误边界 | 组织页面 Layout + 子页面导航 | | Query | 客户端/服务端共享的请求缓存层 | 拉取 GitHub Issues 作为知识库条目 | | Store | 极轻量的响应式状态容器 | 记录用户本地过滤条件和偏好 |

2. 使用 TanStack Start 初始化项目

  1. 安装脚手架

    pnpm create tanstack@latest tanstart-demo
    cd tanstart-demo
    pnpm install
    

    选择 start 模板、routerquery 插件,勾选 typescripttailwind 可快速获得常用配置。

  2. 开发与构建命令

    pnpm dev        # 启动带 HMR 的本地服务器,默认 http://localhost:3000
    pnpm lint       # TanStack Start 默认集成 eslint + prettier
    pnpm build      # 生成 SSR 包,可通过 pnpm preview / fly deploy 体验
    pnpm test       # 如选择 Vitest,会自动生成 vitest.config.ts
    
  3. 项目结构速览

    src/
      routes/
        __root.tsx       # 根布局,放置 Layout、QueryClientProvider
        index.tsx        # 首页
        guides.tsx       # 自定义子路由
      server/
        context.ts       # 服务端 context(headers、db 实例)
      styles/
        globals.css
    

    Start 将 Router、Query、Store 挂接到 src/routes 中的 React 组件里,SSR/SSG 自动处理。

3. 使用 TanStack Router 构建多层页面

  1. 定义根布局 src/routes/__root.tsx

    import { Outlet, Link } from '@tanstack/react-router'
    import { QueryClientProvider } from '@tanstack/react-query'
    import { queryClient } from '../services/query-client'
    
    export const Route = createRootRoute({
      component: () => (
        <QueryClientProvider client={queryClient}>
          <div className="min-h-screen bg-slate-950 text-slate-100">
            <header className="border-b border-slate-800 px-6 py-4 flex gap-4">
              <Link to="/" className="[&.active]:text-emerald-400">首页</Link>
              <Link to="/guides">知识库</Link>
            </header>
            <main className="px-6 py-8">
              <Outlet />
            </main>
          </div>
        </QueryClientProvider>
      ),
    })
    
  2. 配置子路由

    • 首页 src/routes/index.tsx 显示简介。
    • 知识库 src/routes/guides.tsx 依赖 loader(beforeLoadloader)读取搜索参数,并使用 TanStack Store 存储过滤器。
    export const Route = createFileRoute('/guides')({
      component: GuidesPage,
    })
    

Router 的优势在于:声明式 loader + suspense 支持,URL 与状态天然同步,不需要额外的 react-router 包装。

4. 集成 TanStack Query 拉取远程数据

  1. 配置 Query Client src/services/query-client.ts

    import { QueryClient } from '@tanstack/react-query'
    
    export const queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          staleTime: 1000 * 60,
          retry: 1,
          gcTime: 1000 * 60 * 5,
        },
      },
    })
    
  2. 创建复用的查询描述

    import { queryOptions } from '@tanstack/react-query'
    
    const repoOwner = 'tanstack'
    const repoName = 'router'
    
    export const guideListQuery = (label: string) =>
      queryOptions({
        queryKey: ['guides', label],
        queryFn: async () => {
          const res = await fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/issues?labels=${label}`)
          if (!res.ok) throw new Error('加载失败')
          return res.json() as Promise<Array<{ id: number; title: string; html_url: string }>>
        },
      })
    
  3. 在路由组件中使用

    import { useQuery } from '@tanstack/react-query'
    import { guideListQuery } from '../services/queries'
    import { useGuideStore } from '../stores/guide-store'
    
    function GuidesPage() {
      const { label } = useGuideStore()
      const { data, isLoading } = useQuery(guideListQuery(label))
    
      if (isLoading) return <p>同步 GitHub 数据中...</p>
    
      return (
        <ul className="space-y-3">
          {data?.map((issue) => (
            <li key={issue.id} className="rounded border border-slate-800 p-4">
              <a href={issue.html_url} target="_blank" rel="noreferrer">{issue.title}</a>
              <p className="text-xs text-slate-400">Issue #{issue.id}</p>
            </li>
          ))}
        </ul>
      )
    }
    

TanStack Query 的 queryOptions 能与 Router 的 loader 共享缓存:在服务器端调用 queryClient.ensureQueryData(guideListQuery(label)) 后,客户端便可直接复用数据避免二次请求。

5. 使用 TanStack Store 管理本地偏好

TanStack Store 是无依赖、响应式的原子状态容器,像一个轻量版的 signals。

  1. 创建 store src/stores/guide-store.ts

    import { Store } from '@tanstack/store'
    
    type GuideState = {
      label: string
      displayMode: 'card' | 'list'
    }
    
    export const guideStore = new Store<GuideState>({
      label: 'good first issue',
      displayMode: 'list',
    })
    
    export const useGuideStore = () => {
      const [state, setState] = React.useSyncExternalStore(
        (listener) => guideStore.subscribe(listener),
        () => guideStore.state,
      )
      const updateLabel = (label: string) => guideStore.setState((prev) => ({ ...prev, label }))
      const toggleDisplay = () =>
        guideStore.setState((prev) => ({
          ...prev,
          displayMode: prev.displayMode === 'list' ? 'card' : 'list',
        }))
    
      return { ...state, updateLabel, toggleDisplay }
    }
    
  2. 在组件中消费

    function GuideToolbar() {
      const { label, updateLabel, displayMode, toggleDisplay } = useGuideStore()
      return (
        <div className="flex items-center gap-3 mb-6">
          <select value={label} onChange={(e) => updateLabel(e.target.value)}>
            <option value="good first issue">入门</option>
            <option value="help wanted">进阶</option>
          </select>
          <button onClick={toggleDisplay}>视图:{displayMode}</button>
        </div>
      )
    }
    

Store 的 setState 可用于路由 loader 中(同步初始 URL 查询参数),也可在服务端渲染时根据 cookies 注入用户偏好。

6. 将 Router、Query、Store 串联成可复用模式

  1. 在 Loader 中预取数据

    export const Route = createFileRoute('/guides')({
      loader: async ({ context, params, search }) => {
        const label = search.label ?? guideStore.state.label
        guideStore.setState((prev) => ({ ...prev, label }))
        await context.queryClient.ensureQueryData(guideListQuery(label))
        return { label }
      },
      component: GuidesPage,
    })
    
    • context.queryClient 来自 router 的 server context,可在 src/router.ts 创建。
    • Loader 返回的 label 可通过 Route.useLoaderData() 获取,用于在组件中初始化 store。
  2. 表单 + Action 示例

    • src/routes/contact.tsx 中定义 action 调用 server 函数,并在客户端使用 <Form /> 组件提交数据。
    • 成功后用 router.navigate({ to: '/guides', search: { label: 'custom' } }) 跳转到新的 Query。
  3. 错误和边界处理

    • Start 默认启用 Error Boundary:在 createFileRoute 中传入 errorComponent
    • 当 Query 请求失败时,可以抛出错误交由 Router 处理,统一渲染「重试」按钮。

7. 开发流程与部署建议

  1. 本地开发

    • 运行 pnpm dev --host 方便移动端调试。
    • 利用 tanstack.config.ts 定制 routes 生成逻辑(如 alias 或多语言)。
  2. 测试与质量保障

    • 通过 pnpm test 调起 Vitest,对 store 与 hooks 编写单元测试。
    • 使用 Playwright 检查 Router 的关键跳转和 Query 的 Loading 状态。
  3. 部署

    • 默认输出为 Node SSR;可部署到 Vercel、Fly.io 或自建 Node 服务器。
    • 若需要边缘部署,可开启基于 nitro 的适配器或使用 Cloudflare Workers(TanStack Start 提供 template)。
  4. 配置建议

    • .env 中放置 GitHub Token,使用 context.env 在 loader 中访问,提升 API 速率。
    • 借助 @tanstack/router-devtools@tanstack/react-query-devtools 观察 Query 状态,开发阶段以懒加载方式引入。

8. 深入探索的路线图

  1. 集成 TanStack Form:可为 Contact 表单添加 Zod 校验。
  2. 使用 File-based RPC:Start 可以把 src/server 里的函数直接暴露给客户端,利用 Router 的 action 机制调用。
  3. 联动其他后端:通过 context 注入 Prisma/Drizzle,利用 Query + Router loader 构造全栈 CRUD。

完成以上步骤后,你已经拥有了一个具备路由、数据获取、状态管理、服务器行为的 TanStack Start 项目模板,可以直接扩展为知识库、仪表盘或 SaaS Admin。动手实践并根据团队规范迭代,即可在实际业务中享受 TanStack 生态的高可组合性。

comments.logDiscussion Thread
./comments --show-all

讨论区

./loading comments...