TanStack Router 完全指南

相关文档: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 有类型系统和验证支持,这是与其他路由器

分享

官方文档:https://tanstack.com/router/latest
适用版本:TanStack Router 1.x(2026-05-07 核实)

相关文档:TypeScript完全指南 TanStack Query 完全指南 Vue3入门


1. 基础概念

TanStack Router 是什么

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

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

两种路由模式

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

安装

npm install @tanstack/react-router

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

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

2. 代码式路由

完整示例

// 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;
  }
}
// src/main.tsx
import { RouterProvider } from "@tanstack/react-router";
import { router } from "./router";

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

3. 文件式路由(推荐)

Vite 配置

// 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)

根布局

// 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>,
});

普通页面路由

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

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

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

4. 路径参数

// 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 有类型系统和验证支持,这是与其他路由器的重要区别。

基础使用

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 的写法

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

// 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 配合(推荐)

// 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. 导航

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 — 编程式导航

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)

// 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,
});

嵌套路由

// 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 中访问。

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

10. 常用代码段

认证守卫

// 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,
});

登录后跳转原页面

// 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 页面

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

11. 最佳实践

将 queryOptions 与路由 Loader 配合使用

// 定义一次 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 中,这样刷新页面、分享链接都能保持状态:

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

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

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

路由级数据(页面必须的初始数据)放到 Loader,组件级数据(用户交互触发的)用 useQuery:

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

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

12. 踩坑与注意事项

路径参数默认都是 string

路径参数($userId)总是字符串类型,需要手动转换为数字:

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

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

// 替换所有 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 自动验证并提供类型安全。

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(先渲染空壳,再请求数据)。

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

Linkpreload 属性预加载路由:鼠标悬停时预加载目标路由的 loader 数据,点击时路由切换几乎无感延迟。

<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)全部消失。

原因: navigatesearch 参数默认替换所有 search params,不是 merge。

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

// 错误:替换全部 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。

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 命令生成和验证路由树。


参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog