> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# TanStack Router 完全指南
- URL: https://blog.vercanti.com/tanstack-router-wan-quan-zhi-nan/
- Published: 2026-08-28T14:35:21.000Z
- Updated: 2026-08-28T14:58:37.000Z
- Description: 相关文档：TypeScript完全指南(/typescript-wan-quan-zhi-nan/) TanStack Query 完全指南(/tanstack-query-wan-quan-zhi-nan/) Vue3入门(/vue-3-ru-men-zhi-nan/) TanStack Router 是一个完全类型安全的前端路由库，支持 React（稳定）和 Vue（实验性）。与 React Router / Vue Router 的核心区别在于： TanStack Router 的 Search Params 有类型系统和验证支持，这是与其他路由器
- Author: yellowdog
- Tags: 前端开发, TanStack

> 官方文档：<https://tanstack.com/router/latest>  
> 适用版本：TanStack Router 1.x（2026-05-07 核实）

相关文档：[TypeScript完全指南](https://blog.vercanti.com/typescript-wan-quan-zhi-nan/) [TanStack Query 完全指南](https://blog.vercanti.com/tanstack-query-wan-quan-zhi-nan/) [Vue3入门](https://blog.vercanti.com/vue-3-ru-men-zhi-nan/)

---

## 1\. 基础概念

### TanStack Router 是什么

TanStack Router 是一个完全类型安全的前端路由库，支持 React（稳定）和 Vue（实验性）。与 React Router / Vue Router 的核心区别在于：

- 路由定义、路径参数、Search Params 全部类型推断
- 内置 Loader（路由级数据预加载）
- 内置 Search Params 序列化和验证（支持 Zod）
- 与 TanStack Query 深度集成

### 两种路由模式

| 模式    | 说明                            |
| ----- | ----------------------------- |
| 代码式路由 | 手动用 createRoute() 定义路由，适合中小项目 |
| 文件式路由 | 基于文件结构自动生成路由，约定优于配置，推荐大项目使用   |

### 安装

```bash
npm install @tanstack/react-router

# 文件式路由需要额外安装 Vite 插件
npm install -D @tanstack/router-plugin

# 配合 TanStack Query
npm install @tanstack/react-router @tanstack/react-query

```

---

## 2\. 代码式路由

### 完整示例

```tsx
// src/router.tsx
import {
  createRouter,
  createRoute,
  createRootRoute,
  Outlet,
  Link,
} from "@tanstack/react-router";

// 1. 定义根路由（公共布局）
const rootRoute = createRootRoute({
  component: () => (
    <div>
      <nav>
        <Link to="/">首页</Link>
        <Link to="/users">用户列表</Link>
      </nav>
      <Outlet />  {/* 子路由渲染位置 */}
    </div>
  ),
});

// 2. 定义子路由
const indexRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: "/",
  component: () => <h1>首页</h1>,
});

const usersRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: "/users",
  component: UsersPage,
});

// 带动态参数的路由
const userDetailRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: "/users/$userId",  // $userId 是路径参数
  component: UserDetailPage,
});

// 3. 组合路由树
const routeTree = rootRoute.addChildren([
  indexRoute,
  usersRoute,
  userDetailRoute,
]);

// 4. 创建路由器
export const router = createRouter({ routeTree });

// 5. TypeScript 声明（路由类型注册）
declare module "@tanstack/react-router" {
  interface Register {
    router: typeof router;
  }
}

```

```tsx
// src/main.tsx
import { RouterProvider } from "@tanstack/react-router";
import { router } from "./router";

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

```

---

## 3\. 文件式路由（推荐）

### Vite 配置

```ts
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";

export default defineConfig({
  plugins: [
    TanStackRouterVite(),  // 自动扫描 src/routes/ 生成路由
    react(),
  ],
});

```

### 文件结构约定

```
src/routes/
  __root.tsx          → 根布局（必须）
  index.tsx           → /
  about.tsx           → /about
  users/
    index.tsx         → /users
    $userId.tsx       → /users/:userId（动态参数）
    _layout.tsx       → 布局路由（不影响 URL）
  _auth/
    login.tsx         → /login（带 _auth 布局）
  (admin)/
    dashboard.tsx     → /dashboard（括号内不影响 URL）

```

### 根布局

```tsx
// src/routes/__root.tsx
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";

export const Route = createRootRoute({
  component: () => (
    <>
      <nav>
        <Link to="/" activeProps={{ style: { fontWeight: "bold" } }}>
          首页
        </Link>
        <Link to="/users">用户</Link>
      </nav>
      <Outlet />
    </>
  ),
  notFoundComponent: () => <div>404 页面不存在</div>,
});

```

### 普通页面路由

```tsx
// src/routes/users/index.tsx
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/users/")({
  component: UsersPage,
});

function UsersPage() {
  return <div>用户列表</div>;
}

```

---

## 4\. 路径参数

```tsx
// src/routes/users/$userId.tsx
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/users/$userId")({
  component: UserDetailPage,
});

function UserDetailPage() {
  // 完全类型安全，userId 的类型是 string
  const { userId } = Route.useParams();
  return <div>用户 ID：{userId}</div>;
}

```

---

## 5\. Search Params（查询参数）

TanStack Router 的 Search Params 有类型系统和验证支持，这是与其他路由器的重要区别。

### 基础使用

```tsx
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";

// 定义 Search Params 的结构和验证
const searchSchema = z.object({
  page: z.number().int().min(1).default(1),
  pageSize: z.number().int().min(1).max(100).default(20),
  keyword: z.string().optional(),
  status: z.enum(["active", "inactive"]).optional(),
});

export const Route = createFileRoute("/users/")({
  validateSearch: searchSchema,  // 传入 zod schema 自动验证
  component: UsersPage,
});

function UsersPage() {
  // search 完全类型安全
  const { page, pageSize, keyword } = Route.useSearch();

  const navigate = Route.useNavigate();

  const setPage = (newPage: number) => {
    navigate({ search: (prev) => ({ ...prev, page: newPage }) });
  };

  return (
    <div>
      <p>第 {page} 页</p>
      <button onClick={() => setPage(page + 1)}>下一页</button>
    </div>
  );
}

```

### 不使用 Zod 的写法

```tsx
export const Route = createFileRoute("/users/")({
  validateSearch: (search: Record<string, unknown>) => ({
    page: Number(search.page ?? 1),
    keyword: String(search.keyword ?? ""),
  }),
  component: UsersPage,
});

```

---

## 6\. Loader — 路由级数据预加载

Loader 在组件渲染前执行，确保数据准备好后再渲染页面，避免加载状态闪烁。

### 基础 Loader

```tsx
// src/routes/users/$userId.tsx
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/users/$userId")({
  loader: async ({ params }) => {
    // params.userId 类型安全
    const user = await fetchUser(params.userId);
    return { user };  // 返回的数据可在组件中通过 Route.useLoaderData() 获取
  },
  component: UserDetailPage,
  pendingComponent: () => <div>加载中...</div>,   // loader 执行期间显示
  errorComponent: ({ error }) => <div>{error.message}</div>,
});

function UserDetailPage() {
  const { user } = Route.useLoaderData();
  return <div>{user.name}</div>;
}

```

### 与 TanStack Query 配合（推荐）

```tsx
// src/routes/users/$userId.tsx
import { createFileRoute } from "@tanstack/react-router";
import { queryClient } from "@/lib/queryClient";
import { userQueryOptions } from "@/hooks/useUser";

export const Route = createFileRoute("/users/$userId")({
  // 在路由进入时预取数据，存入 QueryClient 缓存
  loader: ({ params }) =>
    queryClient.ensureQueryData(userQueryOptions(Number(params.userId))),

  component: UserDetailPage,
});

function UserDetailPage() {
  const { userId } = Route.useParams();
  // 因为 loader 已经预取，这里立即有数据，不会 pending
  const { data: user } = useQuery(userQueryOptions(Number(userId)));
  return <div>{user?.name}</div>;
}

// src/hooks/useUser.ts
export const userQueryOptions = (id: number) =>
  queryOptions({
    queryKey: ["users", id],
    queryFn: () => fetchUser(id),
  });

```

---

## 7\. 导航

### Link 组件

```tsx
import { Link } from "@tanstack/react-router";

// 基础导航
<Link to="/users">用户列表</Link>

// 带路径参数
<Link to="/users/$userId" params={{ userId: "42" }}>
  用户详情
</Link>

// 带 Search Params
<Link to="/users" search={{ page: 2, keyword: "alice" }}>
  第 2 页
</Link>

// 激活状态样式
<Link
  to="/users"
  activeProps={{ className: "text-blue-500 font-bold" }}
  inactiveProps={{ className: "text-gray-500" }}
>
  用户
</Link>

// 精确匹配（避免父路由也被激活）
<Link to="/" activeOptions={{ exact: true }}>首页</Link>

```

### useNavigate — 编程式导航

```tsx
import { useNavigate } from "@tanstack/react-router";

function LoginPage() {
  const navigate = useNavigate();

  const handleLogin = async () => {
    await login();
    navigate({ to: "/dashboard", replace: true });  // replace 不留历史记录
  };

  // 带参数
  const goToUser = (id: number) => {
    navigate({
      to: "/users/$userId",
      params: { userId: String(id) },
    });
  };

  // 更新 Search Params（保留其他参数）
  const setPage = (page: number) => {
    navigate({
      search: (prev) => ({ ...prev, page }),
    });
  };
}

```

---

## 8\. 布局路由与嵌套路由

### 布局路由（不影响 URL）

```tsx
// src/routes/_auth.tsx  — 带认证检查的布局路由
import { createFileRoute, redirect, Outlet } from "@tanstack/react-router";

export const Route = createFileRoute("/_auth")({
  beforeLoad: ({ context }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({ to: "/login" });
    }
  },
  component: () => (
    <div className="auth-layout">
      <Sidebar />
      <Outlet />
    </div>
  ),
});

// src/routes/_auth/dashboard.tsx  — 受保护的页面
export const Route = createFileRoute("/_auth/dashboard")({
  component: DashboardPage,
});

```

### 嵌套路由

```tsx
// src/routes/users/$userId.tsx  — 父路由
export const Route = createFileRoute("/users/$userId")({
  component: () => (
    <div>
      <UserHeader />
      <Outlet />  {/* 渲染子路由 */}
    </div>
  ),
});

// src/routes/users/$userId/posts.tsx  — 子路由
export const Route = createFileRoute("/users/$userId/posts")({
  component: UserPostsPage,
});

```

---

## 9\. 路由 Context

Context 用于向路由树传递全局依赖（如 QueryClient、认证状态等），可在 Loader 和 beforeLoad 中访问。

```tsx
// src/router.tsx
import { createRouter } from "@tanstack/react-router";

interface RouterContext {
  queryClient: QueryClient;
  auth: AuthState;
}

export const router = createRouter({
  routeTree,
  context: {
    queryClient: undefined!, // 实际值在 App 中提供
    auth: undefined!,
  },
});

// src/App.tsx
function App() {
  const auth = useAuth();
  return (
    <RouterProvider
      router={router}
      context={{ queryClient, auth }}  // 注入 context
    />
  );
}

```

```tsx
// 在路由中使用 context
export const Route = createFileRoute("/_auth/dashboard")({
  beforeLoad: ({ context }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({ to: "/login" });
    }
  },
  loader: ({ context }) =>
    context.queryClient.ensureQueryData(dashboardQueryOptions),
});

```

---

## 10\. 常用代码段

### 认证守卫

```tsx
// src/routes/__root.tsx
export const Route = createRootRouteWithContext<RouterContext>()({
  component: RootLayout,
});

// src/routes/_protected.tsx
export const Route = createFileRoute("/_protected")({
  beforeLoad: ({ context, location }) => {
    if (!context.auth.token) {
      throw redirect({
        to: "/login",
        search: { redirect: location.href },  // 保存来源页面
      });
    }
  },
  component: Outlet,
});

```

### 登录后跳转原页面

```tsx
// src/routes/login.tsx
const loginSearchSchema = z.object({
  redirect: z.string().optional(),
});

export const Route = createFileRoute("/login")({
  validateSearch: loginSearchSchema,
  component: LoginPage,
});

function LoginPage() {
  const { redirect } = Route.useSearch();
  const navigate = useNavigate();

  const handleLogin = async () => {
    await login();
    navigate({ to: redirect ?? "/dashboard", replace: true });
  };
}

```

### 404 页面

```tsx
// src/routes/__root.tsx
export const Route = createRootRoute({
  component: RootLayout,
  notFoundComponent: () => (
    <div>
      <h1>404 - 页面不存在</h1>
      <Link to="/">回到首页</Link>
    </div>
  ),
});

```

---

## 11\. 最佳实践

### 将 queryOptions 与路由 Loader 配合使用

```ts
// 定义一次 queryOptions，在路由 loader 预取，在组件中使用，避免重复定义
export const userListQueryOptions = (params: UserListParams) =>
  queryOptions({
    queryKey: ["users", "list", params],
    queryFn: () => fetchUsers(params),
    staleTime: 1000 * 60,
  });

// 路由 loader
loader: ({ deps }) =>
  queryClient.ensureQueryData(userListQueryOptions(deps)),

// 组件内
const { data } = useQuery(userListQueryOptions(params));

```

### Search Params 作为唯一状态来源

将筛选条件、分页、排序等状态都存到 Search Params 中，这样刷新页面、分享链接都能保持状态：

```tsx
const { page, keyword, status } = Route.useSearch();

// 修改时用 navigate 更新 URL
navigate({ search: (prev) => ({ ...prev, page: newPage }) });

```

### 避免在组件内部做路由级数据获取

路由级数据（页面必须的初始数据）放到 Loader，组件级数据（用户交互触发的）用 useQuery：

```ts
// Loader：页面进入时必须有的数据
loader: ({ params }) => queryClient.ensureQueryData(userQueryOptions(params.userId)),

// 组件内：用户点击某个 tab 后才需要的数据
const { data: orders } = useQuery({ ...ordersQueryOptions, enabled: activeTab === "orders" });

```

---

## 12\. 踩坑与注意事项

### 路径参数默认都是 string

路径参数（`$userId`）总是字符串类型，需要手动转换为数字：

```tsx
const { userId } = Route.useParams();
const numericId = Number(userId); // 手动转换

```

### Search Params 使用对象传入时替换所有参数

```tsx
// 替换所有 search 参数（原来的参数会丢失）
navigate({ search: { page: 2 } });

// 只更新 page，保留其他参数（推荐）
navigate({ search: (prev) => ({ ...prev, page: 2 }) });

```

### 文件式路由命名规则

| 文件名          | 路由路径    | 说明      |
| ------------ | ------- | ------- |
| index.tsx    | / 或父路径  | 索引路由    |
| about.tsx    | /about  | 普通路由    |
| $id.tsx      | /:id    | 动态参数    |
| \_layout.tsx | 不影响 URL | 布局路由    |
| (group)      | 不影响 URL | 路由分组    |
| \_.tsx       | 通配符     | 捕获未匹配路由 |

---

## 最佳实践

**用 `validateSearch` 为 search params 定义 schema**：未验证的 search params 在用户手动修改 URL 时可能传入意外值导致运行时错误。用 zod schema 声明后，TanStack Router 自动验证并提供类型安全。

```ts
import { z } from 'zod'
export const Route = createFileRoute('/products')({
  validateSearch: z.object({
    page: z.number().int().min(1).default(1),
    q: z.string().optional(),
  }),
})

```

**在 loader 中预取数据，避免组件内 waterfall 请求**：loader 在路由匹配时就开始执行，与渲染并行；组件内 `useEffect` 数据加载在组件挂载后才开始，产生 waterfall（先渲染空壳，再请求数据）。

```ts
export const Route = createFileRoute('/users/$id')({
  loader: ({ params }) => fetchUser(params.id),
  component: () => {
    const user = Route.useLoaderData()
    return <div>{user.name}</div>
  },
})

```

**用 `Link` 的 `preload` 属性预加载路由**：鼠标悬停时预加载目标路由的 loader 数据，点击时路由切换几乎无感延迟。

```tsx
<Link to="/users/$id" params={{ id: '1' }} preload="intent">
  View Profile
</Link>

```

**类型安全路由用 `$inferInput`/`$inferOutput` 获取类型**：TanStack Router 的路由参数和 search params 都有完整的 TypeScript 类型，可以从路由定义中直接推导，无需重复定义。

---

## 常见陷阱

### 陷阱：navigate 覆盖 search params 而非更新

**现象：** 只想更新 `page` 参数，调用 `navigate({ search: { page: 2 } })` 后，其他 search params（如 `q`）全部消失。

**原因：** `navigate` 的 `search` 参数默认替换所有 search params，不是 merge。

**解决：** 使用函数式写法保留现有参数。

```ts
// 错误：替换全部 search params
navigate({ search: { page: 2 } })

// 正确：保留现有参数
navigate({ search: (prev) => ({ ...prev, page: 2 }) })

```

---

### 陷阱：loader 中抛出的错误没有被 errorComponent 捕获

**现象：** loader 中 `throw new Error("not found")` 后，页面白屏而非显示错误 UI。

**原因：** TanStack Router 只捕获通过 `redirect()` 或 `notFound()` 抛出的特殊 throw，普通 Error 对象需要配置 `errorComponent`。

**解决：** 在路由定义中加 `errorComponent`，或统一用 `notFound()` 抛出 404。

```ts
import { notFound } from '@tanstack/react-router'
export const Route = createFileRoute('/users/$id')({
  loader: async ({ params }) => {
    const user = await fetchUser(params.id)
    if (!user) throw notFound()   // 正确：触发 notFoundComponent
    return user
  },
  errorComponent: ({ error }) => <div>Error: {error.message}</div>,
  notFoundComponent: () => <div>User not found</div>,
})

```

---

### 陷阱：文件式路由命名错误导致路由不生效

**现象：** 创建了 `$userId.tsx` 文件但路由没有匹配到 `/:userId`。

**原因：** TanStack Router 文件式路由对文件命名格式敏感，动态参数必须用 `$` 前缀，不能用 `:` 或 `[]` 等其他框架的约定。

**解决：** 严格按照 TanStack Router 的命名规范：`$paramName.tsx`；使用 `tsr generate` 命令生成和验证路由树。

---

## 参见

- [TypeScript完全指南](https://blog.vercanti.com/typescript-wan-quan-zhi-nan/)
- [TanStack Query 完全指南](https://blog.vercanti.com/tanstack-query-wan-quan-zhi-nan/)
- [TanStack Table & Form & Virtual 完全指南](https://blog.vercanti.com/tanstack-table-form-virtual-wan-quan-zhi-nan/)
- [React完全指南](https://blog.vercanti.com/react-wan-quan-zhi-nan/)