Skip to content
常见陷阱与最佳实践

常见陷阱与最佳实践

本章汇总 TanStack Router 开发中常见的陷阱和最佳实践,按类别整理以便速查。


安装与配置

陷阱 说明 正确做法
🚨 Vite 插件顺序错误 react 插件在 tanstackRouter 之前会导致 routeTree.gen.ts 无法生成 tanstackRouter() 必须放在 react() 之前
🚨 忘记类型注册 没有 declare module '@tanstack/react-router' 则所有 Hook 失去类型推断 router.tsx 中完成 Register 接口声明
🚨 Zod v3 .catch() 而非 fallback() .catch() 使输出类型变为 unknown,破坏类型安全 使用 @tanstack/zod-adapterfallback()
💡 启用 autoCodeSplitting 新项目直接从第一天就拆分代码 tanstackRouter({ autoCodeSplitting: true })

路由定义

陷阱 说明 正确做法
🚨 路径参数冲突 posts/new.tsxposts/$postId.tsx 静态段优先,"new" 永远不会作为 postId 使用不同的路径结构或搜索参数区分
🚨 可选路径参数 TanStack Router 不支持 /:param? 用搜索参数代替可选路径参数
🚨 路径无关布局的 URL 误解 _auth/admin/users.tsx 的 URL 是 /users,不是 /admin/users 理解 _ 前缀不参与 URL 构建
💡 虚拟路由减负 loader/beforeLoad 简单的路由可以全用 .lazy.tsx 删除主文件,让插件自动生成虚拟路由

导航与链接

陷阱 说明 正确做法
🚨 对象替换搜索参数 <Link search={{ page: 2 }}> 会丢失所有其他搜索参数 始终用函数式更新:search={(prev) => ({ ...prev, page: 2 })}
🚨 <Link> 用于外部 URL to="https://github.com" 会被当作内部路由 外部链接用原生 <a> 标签
🚨 state 不可持久化 navigate({ state: ... }) 在页面刷新后丢失 需要持久化的状态用搜索参数
💡 始终提供 from LinkuseNavigate 提供 from 以获得最精确的类型推断 from="/current-route"

搜索参数

陷阱 说明 正确做法
🚨 useSearch 标注返回类型 手动类型标注会覆盖推断,导致与实际验证不一致 永远不要标注 useSearch() 的返回值类型
🚨 父路由没有 validateSearch 子路由无法继承搜索参数 需要跨路由共享的参数必须在祖先路由中定义 validateSearch
🚨 loaderDeps 传入整个 search 任何搜索参数变化都会触发 loader 重新执行 loaderDeps 只选择 loader 真正依赖的字段
💡 用 stripSearchParams 清理 URL 默认值不应出现在 URL 中 search: { middlewares: [stripSearchParams(defaults)] }
💡 用 retainSearchParams 保持参数 debugtheme 等应跨导航保留 search: { middlewares: [retainSearchParams(['debug'])] }

Loader 与数据加载

陷阱 说明 正确做法
🚨 beforeLoad 中做耗时操作 beforeLoad 是串行的,会阻塞所有子路由的导航 beforeLoad 只做守卫,耗时数据在 loader 中并行加载
🚨 子路由直接访问父 loader 数据 子 loader 无法访问父 loader 的返回值 需要共享数据时,在父 beforeLoad 获取并放入 context
🚨 Loader 错误未被 Error Boundary 捕获 Loader 错误只能通过路由的 errorComponent 处理 每个路由都应定义 errorComponent
💡 使用 abortController.signal 避免组件卸载后的无效请求 fetch(url, { signal: abortController.signal })
💡 合理设置 staleTime 默认值可能导致数据过频或过旧 根据数据变化频率设定:高频 5-15s,稳定 30s-数分钟

认证与守卫

陷阱 说明 正确做法
🚨 只在组件内检查认证 受保护页面会短暂闪现后再跳转 始终在 beforeLoad 中进行认证检查
🚨 context.auth 初始化为 undefined! 忘记这个断言会导致类型错误 createRouter({ context: { auth: undefined! } })
🚨 混淆 redirect 和真实错误 代码中的 try/catch 可能误吞 redirect() 使用 isRedirect(err) 区分并透传 redirect
💡 三层防御 仅靠前端守卫不够 路由守卫 + UI 条件渲染 + API 鉴权
💡 登录后回跳 用户登录后应返回原页面 redirect({ search: { redirect: location.href } })

嵌套与布局

陷阱 说明 正确做法
🚨 布局路由忘记 <Outlet> 子路由组件不会渲染 每个布局路由都应有 <Outlet>
🚨 多层布局的 beforeLoad 性能 每层 beforeLoad 都串行,多层叠加延迟明显 只在必要的层级设置 beforeLoad
💡 布局路由放共享 UI 侧边栏、Tab 导航等不变的部分放布局路由 随子路由变化的部分放子路由组件

代码分割

陷阱 说明 正确做法
🚨 分割 Loader Loader 已是异步的,再分割会引入双重延迟 只分割 component/errorComponent,除非 loader 超大(10KB+)
🚨 所有路由都手动 .lazy.tsx 增加维护负担 autoCodeSplitting: true 替代手动拆分
💡 配合预加载 懒加载 + intent 预加载 = 用户几乎感知不到延迟 defaultPreload: 'intent'

类型安全

陷阱 说明 正确做法
🚨 不提供 from LinkuseNavigate 的类型推断不够精确 始终提供 from 指明当前路由
🚨 用 as 类型断言 绕过类型检查,可能隐藏真实错误 修复类型定义而非绕过它们
💡 活用 getRouteApi 跨组件访问路由的 loader params search 时保持类型安全 const api = getRouteApi('/path')

性能

建议 说明
⚡ 启用 autoCodeSplitting 减少初始包体积约 50%
⚡ 合理使用 preload: 'intent' 在用户可能点击前预加载数据
⚡ 精确设置 loaderDeps 避免无关搜索参数变化触发 loader
⚡ 使用 abortController 取消组件卸载后的无效请求
⚡ 避免 beforeLoad 中的网络请求 它会阻塞导航(串行),改用 loader(并行)

最佳实践清单

项目初始化

  • Vite 插件顺序正确(tanstackRouter 在 react 之前)
  • 完成 declare module Register 类型注册
  • 启用 autoCodeSplitting: true
  • 安装并配置 Devtools

路由设计

  • 用路径参数标识资源 ID,搜索参数控制展示方式
  • 用路径无关布局(_ 前缀)组织认证和守卫
  • 每个布局路由都有 <Outlet>
  • 每个路由都有 errorComponent(至少全局的)
  • 参数 parse/stringify 双向可逆

搜索参数

  • validateSearch(推荐 Zod + zod-adapter)验证所有搜索参数
  • 导航时用函数式更新器保留现有参数
  • loaderDeps 精确控制 loader 触发条件
  • stripSearchParams 移除 URL 中的默认值

数据加载

  • Loader 中传递 abortController.signal
  • 设置合理的 staleTime
  • pendingComponent 处理加载状态
  • Loader 内部使用 notFound() 处理资源不存在

认证

  • Auth Context → Router Context → beforeLoad 三层模型
  • beforeLoad 中进行认证检查,不只在组件中
  • 使用 isRedirect() 区分 redirect 和真实错误
  • 登录页支持回跳(search: { redirect: location.href }

代码质量

  • 始终给 LinkuseNavigate 提供 from
  • 不标注 useSearch() / useParams() 的返回类型
  • 外部链接用 <a>,不用 <Link>
  • 定期检查构建产物大小(rollup-plugin-visualizer)

生产检查清单

  • autoCodeSplitting 已启用,构建产物合理
  • 所有路由的 errorComponent 已定义
  • Devtools 在生产环境关闭(或仅 staging 内部开放)
  • beforeLoad 中无耗时网络请求
  • 搜索参数默认值已通过 stripSearchParams 清理
  • 认证守卫覆盖所有需要保护的页面
  • API 请求通过 abortController.signal 取消
  • staleTimepreloadStaleTime 基于业务需求合理配置
  • 外部链接使用 <a> 标签,下载链接使用 download 属性
  • 路由类型已正确注册,npm run build 通过类型检查