掌握 TanStack Router:从介绍到实战
在现代前端单页应用(SPA)的开发中,路由是构建用户界面的基石。它负责根据URL加载和展示相应的组件,管理应用的导航状态。随着应用复杂度的提升,传统的客户端路由解决方案有时会面临一些挑战,例如:
- 类型安全问题: 参数、搜索参数等信息在不同路由间传递时缺乏类型约束,容易引发运行时错误。
- 数据加载的复杂性: 将组件渲染与数据加载分离往往导致数据瀑布(data waterfalls),影响用户体验和性能。
- 状态管理的碎片化: 路由状态(如当前路径、参数)与业务状态混杂,难以统一管理。
- 开发体验: 路由配置可能变得冗长且难以维护,尤其在大型项目中。
正是在这样的背景下,TanStack Router 应运而生。作为 TanStack 生态系统的新成员(与 TanStack Query, TanStack Table 等齐名),它继承了 TanStack 系列库的优秀基因——专注于类型安全、强大的功能和卓越的开发者体验。
本文将带你深入探索 TanStack Router,从其核心概念出发,逐步深入到实际应用的各个方面,包括路由定义、导航、强大的数据加载机制、类型安全保障以及一些高级特性,最终帮助你全面掌握这个现代化的路由库。
第一部分:初识 TanStack Router – 为什么选择它?
TanStack Router 是一个基于 React 的、类型安全的、具有内置异步数据加载功能的路由库。它不仅仅是一个简单的路径匹配器,更是一个强大的应用状态管理工具,将 URL、组件渲染和数据加载紧密结合。
选择 TanStack Router 的原因有很多,其中最引人注目的特性包括:
- 卓越的类型安全(TypeScript First): 这是 TanStack Router 最核心的卖点之一。它通过强大的类型推断能力,确保你在定义路由、链接导航、访问参数和搜索参数时,都能得到静态类型检查。这意味着在编译阶段就能发现许多潜在的路由相关错误,大大提升了代码的健壮性和开发效率。你再也不用担心因为手误写错参数名而导致的运行时崩溃。
- 内置的异步数据加载机制: TanStack Router 在路由层面提供了
loader
函数。你可以在路由定义中直接指定在进入该路由之前需要加载的数据。路由会在渲染组件 之前 执行loader
函数,并在数据加载完成后才渲染组件。这彻底解决了数据瀑布问题,提升了页面的加载性能和用户体验。加载状态、错误处理和数据的缓存都可以通过这个机制优雅地管理。 - 强大的嵌套路由和布局支持: TanStack Router 对嵌套路由有着一流的支持。通过层层嵌套的路由定义,可以自然地构建出复杂的UI布局,父级路由可以渲染共享的布局元素,子路由则填充特定区域。这种结构化方式使得大型应用的路由管理更加清晰和可维护。
- 基于文件系统的路由选项 (通过插件/约定): 虽然核心库是配置式的,但结合社区插件(如
generouted
),可以轻松实现基于文件系统的路由约定,就像 Next.js 或 Remix 那样。这种方式进一步简化了路由的定义和管理,特别是对于那些喜欢约定大于配置的开发者。 - 优秀的开发者体验: 提供了直观的 API、详尽的文档以及强大的开发者工具,帮助开发者更容易地理解和调试路由状态。热模块替换(HMR)在路由配置变更时也能良好地工作。
- 与 TanStack 生态的协同: 作为 TanStack 家族的一员,它与 TanStack Query 等库可以无缝集成,共同构建高性能、类型安全的应用。
相比之下,许多传统的路由库(如 React Router v5 或更早版本)在类型安全和数据加载方面表现得比较薄弱,通常需要额外的库或手动实现来弥补这些不足。TanStack Router 则将这些关键功能内置并设计得非常优雅。
第二部分:核心概念与安装
在深入代码之前,让我们先理解 TanStack Router 的几个核心概念。
- Router (路由器实例): 这是路由系统的核心。你需要创建一个
Router
实例,并将你的所有路由定义传递给它。这个实例包含了路由的所有配置、状态以及导航逻辑。 - Route (路由): 每个
Route
对象定义了应用中的一个可访问路径。一个路由通常包含:path
: 匹配 URL 的路径模式(可以是静态路径、动态参数路径)。getParentRoute
: 指定该路由的父级路由,用于构建路由树和实现嵌套。component
: 当该路由被激活时渲染的 React 组件。loader
: 可选的异步函数,用于在进入路由前加载数据。errorComponent
: 可选的组件,用于渲染loader
或组件内部发生的错误。pendingComponent
: 可选的组件,用于在loader
加载数据时显示加载状态。staleHandler
: 数据过期时的处理方式。- 其他配置项,如
validateSearch
,parseParams
,stringifyParams
等。
- Route Tree (路由树): TanStack Router 将你的路由定义组织成一棵树状结构。这棵树的根是
rootRoute
,所有其他路由都是它的子孙。嵌套关系通过getParentRoute
定义。路由树是实现嵌套布局和数据继承的基础。 - Link (链接): 用于在应用内部进行声明式导航。
<Link>
组件类似于 HTML 的<a>
标签,但它会通过 History API 进行导航,而不是触发页面刷新。它支持类型安全的to
,params
,search
等属性。 - useNavigate (导航钩子): 用于进行编程式导航。当你需要在事件处理函数或其他非 JSX 代码中进行导航时,可以使用
useNavigate
钩子获取导航函数。 - Loader Data (加载的数据): 通过路由的
loader
函数加载的数据可以通过useLoaderData
钩子在相应的路由组件中访问。这些数据也是完全类型安全的。 - Search Params (搜索参数): URL 中
?
后面的查询字符串参数。TanStack Router 提供了类型安全的方式来访问和验证搜索参数。 - Route Params (路径参数): URL 中动态的部分,如
/users/:userId
中的:userId
。同样可以通过类型安全的方式访问。
安装:
使用 npm 或 yarn 安装 TanStack Router 及其 React 适配器:
“`bash
npm install @tanstack/react-router
or
yarn add @tanstack/react-router
or
pnpm add @tanstack/react-router
“`
如果你使用 TypeScript,安装类型定义:
“`bash
npm install @tanstack/router-core # core types
or
yarn add @tanstack/router-core
or
pnpm add @tanstack/router-core
“`
第三部分:设置与基本路由
现在,让我们通过一个简单的例子来看看如何设置 TanStack Router 并定义一些基本路由。
首先,创建一个根路由。所有其他路由都将是它的子路由。根路由通常不匹配任何特定路径,它的作用是作为路由树的起点,并可以用来定义全局布局或数据加载器。
“`tsx
// src/routes/__root.tsx
import { createRootRoute } from ‘@tanstack/react-router’
import { TanStackRouterDevtools } from ‘@tanstack/router-devtools’
export const rootRoute = createRootRoute({
// 定义一个根路由组件,可以用来包裹所有子路由的内容,实现布局
component: () => (
<>
{/ 在这里可以放导航栏或其他全局布局元素 /}
{/ 开发者工具,仅在开发环境显示 /}
),
})
“`
接下来,定义一些子路由。假设我们有两个页面:首页 (/
) 和关于页面 (/about
)。
“`tsx
// src/routes/index.tsx
import { createFileRoute } from ‘@tanstack/react-router’
export const Route = createFileRoute(‘/’)({
// 指定父路由,这里是根路由
getParentRoute: () => rootRoute,
component: () => (
欢迎来到首页!
这是应用的首页。
),
})
// src/routes/about.tsx
import { createFileRoute } from ‘@tanstack/react-router’
export const Route = createFileRoute(‘/about’)({
getParentRoute: () => rootRoute,
component: () => (
关于我们
了解更多关于我们公司的信息。
),
})
“`
注意: 上面的例子使用了 createFileRoute
。这是一个便捷函数,通常配合文件系统路由插件使用。如果你不使用文件系统路由插件,你需要使用 createRoute
并手动指定 path
和 getParentRoute
。为了演示基础概念,我们先使用 createFileRoute
简化示例,后面会说明如何使用 createRoute
。
更新: 在不使用文件系统路由插件的标准配置下,你需要手动导入并组合路由。
“`tsx
// src/routes/index.tsx (使用 createRoute)
import { createRoute } from ‘@tanstack/react-router’
import { rootRoute } from ‘./__root’ // 导入根路由
export const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: ‘/’, // 手动指定路径
component: () => (
欢迎来到首页!
这是应用的首页。
),
})
// src/routes/about.tsx (使用 createRoute)
import { createRoute } from ‘@tanstack/react-router’
import { rootRoute } from ‘./__root’ // 导入根路由
export const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: ‘/about’, // 手动指定路径
component: () => (
关于我们
了解更多关于我们公司的信息。
),
})
“`
接下来,将这些路由组合起来并创建 Router 实例。
“`tsx
// src/routeTree.ts (或者你喜欢的任何文件名)
import { Route } from ‘@tanstack/react-router’ // 如果你使用 createFileRoute
// import { indexRoute, aboutRoute } from ‘./routes’ // 如果你使用 createRoute 并手动导入
import { rootRoute } from ‘./routes/__root’ // 导入根路由
// 假设你使用 createFileRoute,这里会自动导入所有文件路由导出的 Route
// 如果手动导入,你需要手动构建路由数组
const routeTree = rootRoute.addChildren([
// indexRoute, // 如果手动导入
// aboutRoute, // 如果手动导入
Route, // index route from file system (if applicable)
Route.createRoute({ // example for about route using createRoute within the tree
path: ‘about’, // Path is relative to parent (‘/’)
component: () => (
关于我们
了解更多关于我们公司的信息。
),
}),
// 更多的子路由…
])
// 在实际项目中,如果你使用了文件系统路由,这一步通常是由插件自动完成的
// 你只需要从一个指定文件导入生成的 routeTree
// export const routeTree = rootRoute.addChildren([
// // … dynamically imported file routes
// ]);
“`
澄清 createFileRoute 与 createRoute 的关系:
createRoute
: 这是核心 API,用于手动定义一个路由对象,你需要 explicitly (明确地) 指定path
和getParentRoute
。createFileRoute
: 这是createRoute
的一个便捷封装,主要用于配合文件系统路由插件使用。当你使用文件系统路由时,插件会扫描你的文件结构(例如src/routes/**/*.tsx
),然后调用createFileRoute
并根据文件名和目录结构自动推断path
和getParentRoute
。例如,src/routes/users/$userId.tsx
会被插件处理,在你导出的routeTree
中生成一个路径为/users/:userId
的路由,其父路由是/users
对应的路由。- 如果你不使用文件系统路由插件,你应该主要使用
createRoute
来手动定义和组织你的路由。上面的示例为了简洁混合使用了两者,但更规范的做法是在一个地方(如src/routeTree.ts
)使用createRoute
来定义所有路由并构建路由树,或者完全依赖文件系统路由插件。
创建 Router 实例并在应用中使用:
“`tsx
// src/main.tsx 或 src/App.tsx
import React from ‘react’
import ReactDOM from ‘react-dom/client’
import { RouterProvider, createRouter } from ‘@tanstack/react-router’
// 导入上面定义的路由树
// import { routeTree } from ‘./routeTree’ // 如果你手动创建了 routeTree
// 如果使用文件系统路由,通常会从一个生成的文件导入
// import { routeTree } from ‘./routeTree.gen’ // 文件系统路由插件通常生成的文件
// 假设你手动创建了 routeTree,你需要将其导入
// 如果你使用了文件系统路由,你需要根据插件的说明导入生成的路由树
// 这里我们假设手动创建了 routeTree 并在 main.tsx 中定义
const routeTree = rootRoute.addChildren([indexRoute, aboutRoute]);
// 创建一个 Router 实例
const router = createRouter({ routeTree })
// 为 Router 实例注册类型
// 这通常放在一个单独的文件中,让 TypeScript 能够找到
declare module ‘@tanstack/react-router’ {
interface Register {
router: typeof router
}
}
// 渲染应用
const rootElement = document.getElementById(‘app’)!
if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement)
root.render(
)
}
“`
至此,我们已经成功设置了 TanStack Router,并定义了基本的首页和关于页面路由。在 __root.tsx
中的 <Outlet />
会根据当前 URL 渲染匹配的子路由组件。
第四部分:导航与类型安全
导航是路由库的核心功能之一。TanStack Router 提供了 <Link>
组件用于声明式导航,以及 useNavigate
钩子用于编程式导航。
声明式导航 – <Link>
<Link>
组件与 React Router 的 Link
类似,但提供了更好的类型安全。
“`tsx
// 在你的导航栏组件或其他地方
import { Link } from ‘@tanstack/react-router’
function NavBar() {
return (
)
}
“`
<Link>
组件的 to
属性是类型安全的。如果你尝试链接到一个不存在的路径,TypeScript 会报错。
路径参数 (params
) 和搜索参数 (search
) 的类型安全:
考虑一个用户详情页面,路径为 /users/:userId
。
“`tsx
// src/routes/users.$userId.tsx (使用 createFileRoute)
import { createFileRoute } from ‘@tanstack/react-router’
import { rootRoute } from ‘./__root’
export const Route = createFileRoute(‘/users/$userId’)({
getParentRoute: () => rootRoute,
component: () => {
// 通过 useRouteContext 或 useLoaderData (如果加载了数据) 访问参数
// 更常用的是 useMatches 来访问当前匹配到的路由的参数
const { userId } = Route.useParams() // 类型安全地访问 userId
const search = Route.useSearch() // 类型安全地访问搜索参数
return (
<div className="p-2">
<h3>用户详情</h3>
<p>用户 ID: {userId}</p>
<p>搜索参数: {JSON.stringify(search)}</p>
</div>
)
},
})
// 在另一个组件中链接到用户详情页
import { Link } from ‘@tanstack/react-router’
function UserList({ users }) {
return (
-
{users.map(user => (
-
{/ 导航到特定用户的详情页 /}
{/ params 属性要求一个对象,其键必须与路由路径中的参数名匹配,并且值类型正确 /}
{user.name}
))}
)
}
“`
在上面的例子中:
Link
的to="/users/$userId"
指定了目标路由模板。params={{ userId: user.id }}
提供了路径参数的值。TypeScript 会检查params
对象是否包含userId
属性,并且其值是否兼容 URL 参数(通常是字符串或数字)。如果你写成了params={{ user: user.id }}
或params={{ userId: 123 }}
(如果 userId 在类型中被定义为字符串),TypeScript 会报错。- 在
users.$userId.tsx
组件中,Route.useParams()
钩子返回一个对象{ userId: string }
(默认情况下参数被视为字符串)。你可以通过validateSearch
或parseParams
在路由定义中进一步细化这些类型。
编程式导航 – useNavigate
当你需要在 JavaScript/TypeScript 代码中触发导航时,使用 useNavigate
钩子。
“`tsx
import { useNavigate } from ‘@tanstack/react-router’
function LoginForm() {
const navigate = useNavigate()
const handleSubmit = async (e) => {
e.preventDefault()
// 处理登录逻辑…
const success = await login(…)
if (success) {
// 登录成功后导航到仪表盘
navigate({ to: ‘/dashboard’ })
// 也可以导航到带有参数的路径
// navigate({ to: ‘/users/$userId’, params: { userId: ‘abc’ } })
// 也可以导航到带有搜索参数的路径
// navigate({ to: ‘/search’, search: { q: ‘react’, page: 1 } })
} else {
// 显示错误信息
}
}
return (
)
}
“`
navigate
函数的参数对象 { to, params, search, replace }
等也是完全类型安全的。这极大地减少了因导航目标错误或参数格式不匹配导致的运行时问题。
类型注册 (重要步骤):
为了让 TypeScript 能够正确地推断出路由的类型(包括 to
属性的合法值、useParams
和 useSearch
的返回值类型等),你需要在全局范围内注册你的 Router 实例类型。这通常在 src/main.tsx
或一个单独的类型定义文件(如 src/router.d.ts
)中完成。
“`typescript
// src/router.d.ts 或 src/main.tsx 中
import { type QueryClient } from ‘@tanstack/react-query’ // 如果使用了 react-query
import { type router } from ‘./main’ // 导入你的 router 实例
declare module ‘@tanstack/react-router’ {
interface Register {
// 将你的 router 实例的类型注册到 @tanstack/react-router 模块中
router: typeof router
}
// 可选: 如果你使用 loader 或 action,并且需要在 context 中传递额外的数据
// interface RouterContext {
// queryClient: QueryClient // 例如,在这里添加 queryClient
// }
}
“`
这个类型注册是实现 TanStack Router 全面类型安全的关键步骤。务必不要遗漏。
第五部分:强大的数据加载 (Loaders)
这是 TanStack Router 最具特色的功能之一。通过在路由定义中添加 loader
函数,你可以将数据获取逻辑与组件定义紧密结合,并确保数据在组件渲染前可用。
Loader 的工作原理:
- 用户点击一个
<Link>
或触发一个navigate
调用。 - Router 匹配目标 URL 到相应的路由及其父路由链。
- 对于匹配到的每个路由,Router 会检查其
loader
函数。 - Router 并行执行所有匹配路由上的
loader
函数。 loader
函数通常是异步的,返回一个 Promise。- 在所有
loader
都成功解析后,Router 将这些数据聚合起来,并开始渲染匹配的路由组件树。 - 在组件内部,可以使用
useLoaderData
钩子访问其对应路由的加载数据。
这种机制确保了在组件开始渲染时,它所需的所有关键数据都已经加载完毕,消除了传统方式中组件首次渲染后才触发数据请求导致的加载闪烁和数据瀑布。
定义 Loader:
Loader 函数通常是一个异步函数,它接收一个对象作为参数,包含当前路由的信息,如 params
, search
, context
等。
“`tsx
// src/routes/users.$userId.tsx
import { createFileRoute } from ‘@tanstack/react-router’
import { rootRoute } from ‘./__root’
// 模拟一个数据获取函数
async function fetchUserById(userId: string) {
console.log(Fetching user ${userId}...
)
await new Promise(resolve => setTimeout(resolve, 500)) // 模拟网络延迟
const users = [{ id: ‘abc’, name: ‘Alice’ }, { id: ‘def’, name: ‘Bob’ }]
const user = users.find(u => u.id === userId)
if (!user) {
throw new Error(User with ID ${userId} not found
)
}
return user
}
export const Route = createFileRoute(‘/users/$userId’)({
getParentRoute: () => rootRoute,
// 定义 loader 函数
loader: async ({ params }) => {
console.log(Loader for user ${params.userId} is running...
)
// 从 params 中获取 userId,因为 loader 是在匹配路由时运行,所以 params 是可用的
return fetchUserById(params.userId) // 返回 Promise
},
// 可选: 定义在 loader 加载数据时显示的组件
pendingComponent: () => (
加载用户数据中…
),
// 可选: 定义在 loader 发生错误时显示的组件
errorComponent: ({ error }) => (
加载用户数据失败:
{error.message}
),
component: () => {
// 使用 useLoaderData 钩子访问 loader 返回的数据
// 注意: useLoaderData 的返回值是完全类型安全的,由 loader 函数的返回类型推断
const user = Route.useLoaderData()
return (
<div className="p-2">
<h3>用户详情</h3>
<p>ID: {user.id}</p>
<p>姓名: {user.name}</p>
</div>
)
},
})
“`
在上面的例子中:
loader
函数接收{ params }
并调用fetchUserById
。fetchUserById
是一个模拟的异步数据请求。pendingComponent
会在loader
Promise 处于 pending 状态时显示。errorComponent
会在loader
Promise 被 reject (抛出错误) 时显示。component
只会在loader
Promise resolve (成功加载数据) 后渲染。- 在
component
中,Route.useLoaderData()
返回的数据就是fetchUserById
成功返回的用户对象,并且 TypeScript 知道这个对象的结构 ({ id: string, name: string }
),允许你安全地访问user.id
和user.name
。
Loader 数据缓存与更新:
TanStack Router 的 loader
函数具有内置的缓存机制。默认情况下,当路由匹配时,如果该路由的 loader 数据已经存在且未过期,Router 会直接使用缓存数据而不会重新执行 loader。这对于性能优化非常重要。
你可以通过 staleTime
(数据多久算过期) 和 gcTime
(数据多久后被垃圾回收) 等选项在路由定义中配置缓存行为,这与 TanStack Query 的概念非常相似。
当路由的参数或搜索参数发生变化时,即使路径模式相同 (如 /users/abc
切换到 /users/def
),Router 也会认为这是不同的路由状态,并会重新执行 loader 来获取新数据。
手动触发 Loader 刷新:
你可以使用 useLoaderData({ key: '...' })
并结合 useMatches
或其他方式来手动触发 loader 的刷新,但这通常不如让 Router 根据参数变化自动处理来得常见。
集成 TanStack Query (可选):
虽然 TanStack Router 的 loader
提供了基本的数据加载和缓存,但对于更复杂的数据管理需求(如数据同步、后台更新、乐观更新等),通常会将其与 TanStack Query 等专业的数据获取库结合使用。
你可以在 loader
函数中直接调用 TanStack Query 的 queryClient.ensureQueryData
或 queryClient.fetchQuery
方法来预加载数据:
“`tsx
// 在 Router Context 中提供 QueryClient
// src/router.d.ts 或其他类型文件
declare module ‘@tanstack/react-router’ {
interface Register {
router: typeof router
}
interface RouterContext {
queryClient: QueryClient // 假设你在 main.tsx 中提供了 QueryClient
}
}
// src/main.tsx
import { QueryClient, QueryClientProvider } from ‘@tanstack/react-query’
const queryClient = new QueryClient()
const router = createRouter({
routeTree,
// 在 context 中提供 queryClient
context: {
queryClient,
},
defaultPreload: ‘intent’, // 可选:在用户悬停 Link 时预加载数据
})
// 渲染应用时包裹 QueryClientProvider
root.render(
)
// src/routes/users.$userId.tsx
import { createFileRoute } from ‘@tanstack/react-router’
import { rootRoute } from ‘./__root’
import { queryOptions } from ‘@tanstack/react-query’ // 推荐使用 queryOptions
// 定义 Query Options
const userQueryOptions = (userId: string) => queryOptions({
queryKey: [‘user’, userId],
queryFn: () => fetchUserById(userId), // 你的数据获取逻辑
staleTime: 1000 * 60 * 5, // 5 minutes
})
export const Route = createFileRoute(‘/users/$userId’)({
getParentRoute: () => rootRoute,
loader: async ({ params, context: { queryClient } }) => {
// 在 loader 中预加载数据,使用 queryClient
// ensureQueryData 会检查缓存,如果数据不存在或过期则 fetch
return queryClient.ensureQueryData(userQueryOptions(params.userId))
},
component: () => {
// 在组件中通过 useQuery 访问数据,它会从缓存中读取 loader 预加载的数据
const { userId } = Route.useParams()
const { data: user } = useQuery(userQueryOptions(userId)) // 类型安全的 user 对象
if (!user) {
// useQuery 可能会在初始加载时是 undefined,虽然 loader 保证了数据存在,
// 但为了健壮性或处理后续更新,可以检查
return <p>Loading or user not found...</p>; // Or handle loading/error states via useQuery's state
}
return (
<div className="p-2">
<h3>用户详情 (Via React Query)</h3>
<p>ID: {user.id}</p>
<p>姓名: {user.name}</p>
</div>
)
},
})
“`
这种模式结合了 TanStack Router 的路由级预加载能力与 TanStack Query 强大的数据管理能力,是构建高性能应用的推荐方式。
第六部分:高级特性
TanStack Router 提供了一些高级特性来处理更复杂的场景。
认证/授权 (Guarding Routes):
你可以在 loader
函数中使用 redirect
或 throw
来阻止用户访问某个路由,实现认证或授权检查。
“`tsx
// 假设有一个函数检查用户是否已登录
async function isAuthenticated() {
// 模拟检查登录状态
await new Promise(resolve => setTimeout(resolve, 200));
return localStorage.getItem(‘isLoggedIn’) === ‘true’;
}
// src/routes/dashboard.tsx
import { createFileRoute, redirect } from ‘@tanstack/react-router’
import { rootRoute } from ‘./__root’
export const Route = createFileRoute(‘/dashboard’)({
getParentRoute: () => rootRoute,
// 在进入 dashboard 路由前检查认证状态
beforeLoad: async ({ context, location }) => {
const authenticated = await isAuthenticated()
if (!authenticated) {
// 如果未登录,重定向到登录页面
throw redirect({
to: ‘/login’,
search: {
redirect: location.href, // 可选: 登录后跳回当前页面
},
})
}
// 如果已登录,继续加载路由
},
component: () => (
仪表盘
欢迎回来,已认证用户!
),
})
// src/routes/login.tsx
import { createFileRoute, useNavigate } from ‘@tanstack/react-router’
import { rootRoute } from ‘./__root’
export const Route = createFileRoute(‘/login’)({
getParentRoute: () => rootRoute,
// 如果已登录,重定向到首页 (防止已登录用户访问登录页)
beforeLoad: async () => {
const authenticated = await isAuthenticated()
if (authenticated) {
throw redirect({ to: ‘/’ })
}
},
component: () => {
const navigate = useNavigate()
const search = Route.useSearch() // 访问 redirect 搜索参数
const handleLogin = () => {
localStorage.setItem('isLoggedIn', 'true');
// 登录成功后重定向到之前尝试访问的页面,或默认到首页
const redirectPath = search.redirect || '/'
navigate({ to: redirectPath })
}
return (
<div className="p-2">
<h3>登录</h3>
<button onClick={handleLogin}>模拟登录</button>
</div>
);
},
})
“`
beforeLoad
钩子在 loader
之前执行,非常适合用于身份验证、权限检查或预加载少量关键数据。
布局路由 (Layout Routes):
嵌套路由自然地支持布局。父级路由的 component
渲染的内容会包裹其子路由的 component
。
“`tsx
// src/routes/__root.tsx (Layout)
import { createRootRoute, Outlet } from ‘@tanstack/react-router’
import NavBar from ‘../components/NavBar’; // 假设有一个导航栏组件
export const rootRoute = createRootRoute({
component: () => (
<>
),
})
// src/routes/dashboard.tsx (Nested Layout)
import { createFileRoute, Outlet } from ‘@tanstack/react-router’
import Sidebar from ‘../components/Sidebar’; // 假设有一个侧边栏组件
// 创建一个父级路由,用于定义仪表盘区域的布局
export const dashboardRoute = createFileRoute(‘/dashboard’)({
getParentRoute: () => rootRoute,
component: () => (
),
})
// src/routes/dashboard/overview.tsx (Child Route)
import { createFileRoute } from ‘@tanstack/react-router’
import { dashboardRoute } from ‘../dashboard’ // 导入父级路由
export const Route = createFileRoute(‘/dashboard/overview’)({
getParentRoute: () => dashboardRoute, // 指定父级路由为 dashboardRoute
component: () => (
仪表盘概览
一些概览信息…
),
})
“`
在这个例子中,/dashboard/overview
路径会渲染:rootRoute
的布局 -> dashboardRoute
的布局 -> overview
路由的组件。
错误处理与 Pending 状态:
除了在 loader
级别定义 errorComponent
和 pendingComponent
,你也可以在路由树的更高层级(如 rootRoute
或父级布局路由)定义它们,作为子路由的默认 fallback。
“`tsx
// src/routes/__root.tsx
import { createRootRoute, Outlet } from ‘@tanstack/react-router’
export const rootRoute = createRootRoute({
component: () => (
<>
{/ … NavBar /}
{/ Devtools … /}
),
// 全局的错误处理组件
errorComponent: ({ error }) => (
出错了!
{error.message}
),
// 全局的加载状态组件
pendingComponent: () => (
应用正在加载中…
)
})
“`
如果一个路由没有定义自己的 errorComponent
或 pendingComponent
,并且其 loader
发生错误或正在加载,Router 会沿着路由树向上查找,使用第一个找到的父级路由定义的 errorComponent
或 pendingComponent
。
第七部分:文件系统路由 (Optional but Recommended)
手动使用 createRoute
构建路由树对于小型应用或理解原理很有帮助,但对于大型应用,基于文件系统的路由约定可以显著提高开发效率。TanStack Router 本身是配置式的,但它被设计为易于集成文件系统路由插件。generouted/react-router
是一个流行的选择。
使用 generouted/react-router
的基本流程:
- 安装插件:
npm install generouted @tanstack/react-router
- 在项目根目录(或指定目录)创建
src/routes
文件夹。 - 在
src/routes
文件夹中创建你的路由文件,例如:src/routes/_app.tsx
: 通常用于根布局或上下文提供。src/routes/index.tsx
: 对应/
路径。src/routes/about.tsx
: 对应/about
路径。src/routes/users/$userId.tsx
: 对应/users/:userId
路径。src/routes/admin/posts.tsx
: 对应/admin/posts
路径。- 使用下划线前缀 (
_
): 表示布局路由,不会直接匹配路径,但会包裹子路由。 - 使用美元符号前缀 (
$
): 表示动态参数。
- 在你的构建配置中集成
generouted
CLI 或 Webpack/Vite 插件,让它扫描src/routes
目录并生成路由树文件(通常是src/router.ts
或src/router.gen.ts
)。 - 在
main.tsx
中导入生成的路由树并创建 Router 实例。
“`typescript
// src/routes/_app.tsx (Example layout)
import { Outlet, createFileRoute } from ‘@tanstack/react-router’;
export const Route = createFileRoute(‘/_app’)({
component: () => (
<>
),
});
// src/routes/index.tsx
import { createFileRoute } from ‘@tanstack/react-router’;
import { Route as AppRoute } from ‘./_app’; // Import parent layout route
export const Route = createFileRoute(‘/’)({
// generouted automatically sets parentRoute to _app if in the same directory or ancestors
// but you can explicitly set it if needed: getParentRoute: () => AppRoute,
component: () => (
Home Page
),
});
// src/routes/users/$userId.tsx
import { createFileRoute } from ‘@tanstack/react-router’;
// Parent route could be src/routes/users.tsx or similar layout route if it exists
export const Route = createFileRoute(‘/users/$userId’)({
loader: async ({ params }) => { / fetch user data / return { id: params.userId, name: ‘…’ } },
component: () => {
const user = Route.useLoaderData();
return
;
},
});
// src/main.tsx
import { createRouter, RouterProvider } from ‘@tanstack/react-router’;
// Import the generated route tree
import { routeTree } from ‘./router’; // This file is generated by generouted
const router = createRouter({ routeTree });
declare module ‘@tanstack/react-router’ {
interface Register {
router: typeof router;
}
}
ReactDOM.createRoot(document.getElementById(‘root’)!).render(
);
“`
文件系统路由极大地简化了路由的定义和组织,使得开发者能够通过直观的文件结构来管理复杂的路由层级。
第八部分:实战与最佳实践
将 TanStack Router 应用于实际项目时,考虑以下最佳实践:
- 统一路由定义: 无论你是手动创建路由树还是使用文件系统路由,都应该有一个集中的地方管理路由定义,避免分散在应用各处。
- 充分利用 Loader: 将组件渲染所需的数据加载逻辑尽可能地放入
loader
中。这不仅能解决数据瀑布,还能利用 Router 的缓存机制。 - Loader 数据与组件分离: Loader 负责获取数据,组件负责使用数据渲染UI。保持这种职责分离,提高代码的可维护性。
- Loader 错误与 Pending 状态处理: 在路由定义中配置
errorComponent
和pendingComponent
,为用户提供更好的加载和错误反馈。 - 类型安全是朋友: 始终利用 TypeScript 的强大功能。确保正确注册 Router 类型,并注意
Link
、navigate
、useParams
、useSearch
和useLoaderData
的类型约束。不要绕过类型系统。 - 使用 Context 传递全局依赖: 如果你的
loader
函数需要访问一些全局的依赖(如 API 客户端、认证信息、QueryClient 等),可以通过createRouter
的context
选项传递,并在loader
参数中接收。 - 利用 Layout 路由: 嵌套路由结合布局组件,可以有效地组织和复用页面结构。
- 文件系统路由的优势: 对于中大型应用,强烈推荐使用文件系统路由插件(如
generouted
),它能让路由结构更加清晰,减少手动配置的工作量。 - 开发者工具: 使用
@tanstack/router-devtools
可以可视化路由树、当前状态、Loader 数据等信息,极大地提高了调试效率。
第九部分:总结
TanStack Router 是一个为现代 React 应用量身打造的路由库。它通过内置的类型安全、强大的数据加载机制和灵活的嵌套路由支持,解决了传统路由方案中的痛点。虽然相较于一些老牌路由库,它的概念可能略有不同,但其带来的开发效率提升和应用性能优化是显而易见的。
从定义类型安全的路由和导航,到利用 loader
预加载数据消除瀑布,再到构建复杂的布局和处理认证授权,TanStack Router 提供了一套全面且优雅的解决方案。结合文件系统路由插件,它可以进一步简化大型项目的路由管理。
如果你正在寻找一个现代化、类型安全、注重性能和开发者体验的 React 路由方案,TanStack Router 绝对值得你深入学习和采用。
第十部分:进一步学习资源
- 官方文档: https://tanstack.com/router/latest 这是最权威和详细的学习资源。
- GitHub 仓库: https://github.com/TanStack/router 查看源代码、提交 issue、参与社区。
- 示例: 官方文档和 GitHub 仓库通常包含各种用例的示例代码。
- Generouted: https://generouted.com/ 文件系统路由插件的文档。
通过阅读本文,你应该对 TanStack Router 的核心概念、设置、使用方法以及一些高级特性有了全面的了解。现在,是时候在你的下一个项目中尝试它,亲身体验其带来的便利与强大了!