跳到主要内容

Router 基础

TanStack Router 第一眼看上去像是“又一个文件路由方案”。如果只看到这里,你大概率会低估它。

它真正想做的不是帮你少写几行路由配置,而是把“路由树、URL、参数、导航、数据加载”整成一套强类型系统。

1. 它到底在解决什么

官方 Overview 把这些能力摆在了最前面:

  • 100% inferred TypeScript support
  • typesafe navigation
  • nested routing
  • built-in route loaders
  • JSON-first search params
  • path and search parameter schema validation

这里的关键词不是“功能多”,而是“同一套类型系统贯穿始终”。

2. 文件路由是主线

官方 File-Based Routing 文档写得很明确:这是推荐方式。

原因也很实际:

  • 路由结构跟 URL 更贴近
  • 自动代码分割更自然
  • 大型项目里更容易组织
  • 类型链接可以由插件/CLI 生成

它不是单一目录流派

TanStack Router 的文件路由有个很有意思的点:除了目录嵌套,它还大量使用文件名里的 . 来表达层级。

比如:

  • settings.profile.tsx
  • account.overview.tsx

官方文档专门解释了这一点,因为它能减少为浅层嵌套反复新建目录的噪音。第一次看会有点怪,用顺之后其实很省事。

一个最小 Router 配置

import {
Outlet,
RouterProvider,
createRootRoute,
createRoute,
createRouter,
} from '@tanstack/react-router'

const rootRoute = createRootRoute({
component: () => (
<div>
<h1>TanStack Router Demo</h1>
<Outlet />
</div>
),
})

const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: () => <div>首页</div>,
})

const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
component: () => <div>文章页</div>,
})

const routeTree = rootRoute.addChildren([indexRoute, postsRoute])

const router = createRouter({
routeTree,
defaultPreload: 'intent',
})

export function App() {
return <RouterProvider router={router} />
}

如果你之前只接触过 React Router,这段代码里最值得注意的是:

  • 路由树本身就是类型系统的一部分
  • getParentRoute 在 code-based 模式里很关键
  • defaultPreload 这种体验级配置可以直接收在 router 上

文件路由版本会更接近真实项目

// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
component: PostPage,
})

function PostPage() {
const { postId } = Route.useParams()

return <div>文章 ID: {postId}</div>
}

3. File-based 和 Code-based 可以混用

这是它比很多“文件路由即唯一真理”的框架更灵活的地方。

官方 Overview 明说:

  • 支持 file-based
  • 支持 code-based
  • 两种方式可以同时存在

这点很重要。因为真实项目里总会有一些路由树不是很好用文件系统表达,或者你就是想在某段范围内保留手写控制权。Router 在这件事上没有很教条。

4. 根路由、布局路由、嵌套路由

理解 Router,先别急着背 API,先把层级关系看清楚:

  • Root Route 是整棵树的根
  • 下面继续挂布局路由和页面路由
  • 子路由天然继承父级上下文、参数和布局边界

这和现代 React 路由体系的大方向一致,但 TanStack 把类型向上传递、向下继承这件事做得更强。尤其在多层搜索参数和上下文传递时,差别会很明显。

一个嵌套路由例子

// src/routes/dashboard.tsx
import { Outlet, createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/dashboard')({
component: DashboardLayout,
})

function DashboardLayout() {
return (
<section>
<aside>侧边栏</aside>
<main>
<Outlet />
</main>
</section>
)
}
// src/routes/dashboard.settings.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/dashboard/settings')({
component: () => <div>设置页</div>,
})

5. Params 不是“能拿到”就算完

很多路由库做到“我能拿到 id 参数”就结束了。

TanStack Router 更在意的是:

  • 参数要有类型
  • 最好能校验
  • 序列化和反序列化要可控

带参数校验的例子

import { z } from 'zod'
import { createFileRoute } from '@tanstack/react-router'

const productParams = z.object({
productId: z.string().uuid(),
})

export const Route = createFileRoute('/products/$productId')({
params: {
parse: (raw) => productParams.parse(raw),
},
component: ProductPage,
})

function ProductPage() {
const { productId } = Route.useParams()

return <div>{productId}</div>
}

当你的参数不只是一个简单字符串,而是更复杂的业务标识、枚举、分页边界时,Router 会显得更像工程工具,而不是只负责跳页面的胶水层。

6. 它的类型系统为什么有吸引力

官方 Type Safety 文档里反复强调一件事:它不是“用 TypeScript 写的路由库”,而是努力让路由体验本身尽量被推导出来。

这件事的价值,在小 demo 里未必明显;一旦项目长大,区别就出来了:

  • 你点导航时更不容易拼错路径
  • 你改参数结构时,编译期就能看见影响范围
  • search params 不再只是散落在项目里的字符串协议

7. 它适合什么项目

很适合

  • URL 是产品状态一部分的应用
  • 有复杂筛选、分页、表格、搜索条件的后台
  • 对类型安全要求高的 React 项目
  • 想把路由层和数据层配合得更紧

没那么必要

  • 页面很少、状态很轻的小站
  • 团队几乎不用 TypeScript
  • 路由层本来就不是复杂度来源

8. 第一次上手建议

  1. 先看路由文件命名规则
  2. 再看根路由和嵌套路由的关系
  3. 最后找一个带 params + search + loader 的真实页面

这三样一旦看顺,后面的预加载、SSR、路由上下文都会容易很多。