tanstack-router

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TanStack Router

TanStack Router

Type-safe, file-based routing for React SPAs with route-level data loading and TanStack Query integration

为React SPA提供支持路由级数据加载和TanStack Query集成的类型安全、基于文件的路由方案

Quick Start

快速开始

Last Updated: 2026-01-09 Version: @tanstack/react-router@1.146.2
bash
npm install @tanstack/react-router @tanstack/router-devtools
npm install -D @tanstack/router-plugin
最后更新: 2026-01-09 版本: @tanstack/react-router@1.146.2
bash
npm install @tanstack/react-router @tanstack/router-devtools
npm install -D @tanstack/router-plugin

Optional: Zod validation adapter

可选:Zod验证适配器

npm install @tanstack/zod-adapter zod

**Vite Config** (TanStackRouterVite MUST come before react()):
```typescript
// vite.config.ts
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [TanStackRouterVite(), react()], // Order matters!
})
File Structure:
src/routes/
├── __root.tsx         → createRootRoute() with <Outlet />
├── index.tsx          → createFileRoute('/')
└── posts.$postId.tsx  → createFileRoute('/posts/$postId')
App Setup:
typescript
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen' // Auto-generated by plugin

const router = createRouter({ routeTree })
<RouterProvider router={router} />

npm install @tanstack/zod-adapter zod

**Vite配置**(TanStackRouterVite必须在react()之前):
```typescript
// vite.config.ts
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [TanStackRouterVite(), react()], // 顺序很重要!
})
文件结构:
src/routes/
├── __root.tsx         → 使用<Outlet />创建根路由createRootRoute()
├── index.tsx          → 创建文件路由createFileRoute('/')
└── posts.$postId.tsx  → 创建文件路由createFileRoute('/posts/$postId')
应用设置:
typescript
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen' // 由插件自动生成

const router = createRouter({ routeTree })
<RouterProvider router={router} />

Core Patterns

核心模式

Type-Safe Navigation (routes auto-complete, params typed):
typescript
<Link to="/posts/$postId" params={{ postId: '123' }} />
<Link to="/invalid" /> // ❌ TypeScript error
Route Loaders (data fetching before render):
typescript
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => ({ post: await fetchPost(params.postId) }),
  component: ({ useLoaderData }) => {
    const { post } = useLoaderData() // Fully typed!
    return <h1>{post.title}</h1>
  },
})
TanStack Query Integration (prefetch + cache):
typescript
const postOpts = (id: string) => queryOptions({
  queryKey: ['posts', id],
  queryFn: () => fetchPost(id),
})

export const Route = createFileRoute('/posts/$postId')({
  loader: ({ context: { queryClient }, params }) =>
    queryClient.ensureQueryData(postOpts(params.postId)),
  component: () => {
    const { postId } = Route.useParams()
    const { data } = useQuery(postOpts(postId))
    return <h1>{data.title}</h1>
  },
})

类型安全导航(路由自动补全,参数带类型):
typescript
<Link to="/posts/$postId" params={{ postId: '123' }} />
<Link to="/invalid" /> // ❌ TypeScript错误
路由加载器(渲染前获取数据):
typescript
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => ({ post: await fetchPost(params.postId) }),
  component: ({ useLoaderData }) => {
    const { post } = useLoaderData() // 完全带类型!
    return <h1>{post.title}</h1>
  },
})
TanStack Query集成(预取 + 缓存):
typescript
const postOpts = (id: string) => queryOptions({
  queryKey: ['posts', id],
  queryFn: () => fetchPost(id),
})

export const Route = createFileRoute('/posts/$postId')({
  loader: ({ context: { queryClient }, params }) =>
    queryClient.ensureQueryData(postOpts(params.postId)),
  component: () => {
    const { postId } = Route.useParams()
    const { data } = useQuery(postOpts(postId))
    return <h1>{data.title}</h1>
  },
})

Virtual File Routes (v1.140+)

虚拟文件路由(v1.140+)

Programmatic route configuration when file-based conventions don't fit your needs:
Install:
npm install @tanstack/virtual-file-routes
Vite Config:
typescript
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    tanstackRouter({
      target: 'react',
      virtualRouteConfig: './routes.ts', // Point to your routes file
    }),
    react(),
  ],
})
routes.ts (define routes programmatically):
typescript
import { rootRoute, route, index, layout, physical } from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  index('home.tsx'),
  route('/posts', 'posts/posts.tsx', [
    index('posts/posts-home.tsx'),
    route('$postId', 'posts/posts-detail.tsx'),
  ]),
  layout('first', 'layout/first-layout.tsx', [
    route('/nested', 'nested.tsx'),
  ]),
  physical('/classic', 'file-based-subtree'), // Mix with file-based
])
Use Cases: Custom route organization, mixing file-based and code-based, complex nested layouts.

当基于文件的约定不符合需求时,使用编程式路由配置:
安装:
npm install @tanstack/virtual-file-routes
Vite配置:
typescript
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
  plugins: [
    tanstackRouter({
      target: 'react',
      virtualRouteConfig: './routes.ts', // 指向你的路由文件
    }),
    react(),
  ],
})
routes.ts(编程式定义路由):
typescript
import { rootRoute, route, index, layout, physical } from '@tanstack/virtual-file-routes'

export const routes = rootRoute('root.tsx', [
  index('home.tsx'),
  route('/posts', 'posts/posts.tsx', [
    index('posts/posts-home.tsx'),
    route('$postId', 'posts/posts-detail.tsx'),
  ]),
  layout('first', 'layout/first-layout.tsx', [
    route('/nested', 'nested.tsx'),
  ]),
  physical('/classic', 'file-based-subtree'), // 与基于文件的路由混合使用
])
适用场景: 自定义路由组织、混合基于文件和基于代码的路由、复杂嵌套布局。

Search Params Validation (Zod Adapter)

搜索参数验证(Zod适配器)

Type-safe URL search params with runtime validation:
Basic Pattern (inline validation):
typescript
import { z } from 'zod'

export const Route = createFileRoute('/products')({
  validateSearch: (search) => z.object({
    page: z.number().catch(1),
    filter: z.string().catch(''),
    sort: z.enum(['newest', 'oldest', 'price']).catch('newest'),
  }).parse(search),
})
Recommended Pattern (Zod adapter with fallbacks):
typescript
import { zodValidator, fallback } from '@tanstack/zod-adapter'
import { z } from 'zod'

const searchSchema = z.object({
  query: z.string().min(1).max(100),
  page: fallback(z.number().int().positive(), 1),
  sortBy: z.enum(['name', 'date', 'relevance']).optional(),
})

export const Route = createFileRoute('/search')({
  validateSearch: zodValidator(searchSchema),
  // Type-safe: Route.useSearch() returns typed params
})
Why
.catch()
over
.default()
: Use
.catch()
to silently fix malformed params. Use
.default()
+
errorComponent
to show validation errors.

带运行时验证的类型安全URL搜索参数:
基础模式(内联验证):
typescript
import { z } from 'zod'

export const Route = createFileRoute('/products')({
  validateSearch: (search) => z.object({
    page: z.number().catch(1),
    filter: z.string().catch(''),
    sort: z.enum(['newest', 'oldest', 'price']).catch('newest'),
  }).parse(search),
})
推荐模式(带回退的Zod适配器):
typescript
import { zodValidator, fallback } from '@tanstack/zod-adapter'
import { z } from 'zod'

const searchSchema = z.object({
  query: z.string().min(1).max(100),
  page: fallback(z.number().int().positive(), 1),
  sortBy: z.enum(['name', 'date', 'relevance']).optional(),
})

export const Route = createFileRoute('/search')({
  validateSearch: zodValidator(searchSchema),
  // 类型安全:Route.useSearch()返回带类型的参数
})
为什么用
.catch()
而不是
.default()
: 使用
.catch()
自动修复格式错误的参数。使用
.default()
+
errorComponent
展示验证错误。

Error Boundaries

错误边界

Handle errors at route level with typed error components:
Route-Level Error Handling:
typescript
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    if (!post) throw new Error('Post not found')
    return { post }
  },
  errorComponent: ({ error, reset }) => (
    <div>
      <p>Error: {error.message}</p>
      <button onClick={reset}>Retry</button>
    </div>
  ),
})
Default Error Component (global fallback):
typescript
const router = createRouter({
  routeTree,
  defaultErrorComponent: ({ error }) => (
    <div className="error-page">
      <h1>Something went wrong</h1>
      <p>{error.message}</p>
    </div>
  ),
})
Not Found Handling:
typescript
export const Route = createFileRoute('/posts/$postId')({
  notFoundComponent: () => <div>Post not found</div>,
})

在路由级别处理错误,支持带类型的错误组件:
路由级错误处理:
typescript
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    if (!post) throw new Error('未找到文章')
    return { post }
  },
  errorComponent: ({ error, reset }) => (
    <div>
      <p>错误: {error.message}</p>
      <button onClick={reset}>重试</button>
    </div>
  ),
})
默认错误组件(全局回退):
typescript
const router = createRouter({
  routeTree,
  defaultErrorComponent: ({ error }) => (
    <div className="error-page">
      <h1>出问题了</h1>
      <p>{error.message}</p>
    </div>
  ),
})
404处理:
typescript
export const Route = createFileRoute('/posts/$postId')({
  notFoundComponent: () => <div>文章未找到</div>,
})

Authentication with beforeLoad

基于beforeLoad的身份验证

Protect routes before they load (no flash of protected content):
Single Route Protection:
typescript
import { redirect } from '@tanstack/react-router'

export const Route = createFileRoute('/dashboard')({
  beforeLoad: async ({ context }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: '/login',
        search: { redirect: location.pathname }, // Save for post-login
      })
    }
  },
})
Protect Multiple Routes (layout route pattern):
typescript
// routes/(authenticated)/route.tsx - protects all children
export const Route = createFileRoute('/(authenticated)')({
  beforeLoad: async ({ context }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({ to: '/login' })
    }
  },
})
Passing Auth Context (from React hooks):
typescript
// main.tsx - pass auth state to router
function App() {
  const auth = useAuth() // Your auth hook

  return (
    <RouterProvider
      router={router}
      context={{ auth }} // Available in beforeLoad
    />
  )
}

在路由加载前进行保护(避免闪现受保护内容):
单一路由保护:
typescript
import { redirect } from '@tanstack/react-router'

export const Route = createFileRoute('/dashboard')({
  beforeLoad: async ({ context }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({
        to: '/login',
        search: { redirect: location.pathname }, // 登录后跳转回原页面
      })
    }
  },
})
保护多个路由(布局路由模式):
typescript
// routes/(authenticated)/route.tsx - 保护所有子路由
export const Route = createFileRoute('/(authenticated)')({
  beforeLoad: async ({ context }) => {
    if (!context.auth.isAuthenticated) {
      throw redirect({ to: '/login' })
    }
  },
})
传递认证上下文(来自React钩子):
typescript
// main.tsx - 将认证状态传递给路由
function App() {
  const auth = useAuth() // 你的认证钩子

  return (
    <RouterProvider
      router={router}
      context={{ auth }} // 在beforeLoad中可用
    />
  )
}

Known Issues Prevention

已知问题预防

This skill prevents 20 documented issues:
本方案可预防20种已记录的问题:

Issue #1: Devtools Dependency Resolution

问题 #1: 开发者工具依赖解析

  • Error: Build fails with
    @tanstack/router-devtools-core
    not found
  • Fix:
    npm install @tanstack/router-devtools
Issue #2: Vite Plugin Order (CRITICAL)
  • Error: Routes not auto-generated,
    routeTree.gen.ts
    missing
  • Fix: TanStackRouterVite MUST come before react() in plugins array
  • Why: Plugin processes route files before React compilation
Issue #3: Type Registration Missing
  • Error:
    <Link to="...">
    not typed, no autocomplete
  • Fix: Import
    routeTree
    from
    ./routeTree.gen
    in main.tsx to register types
Issue #4: Loader Not Running
  • Error: Loader function not called on navigation
  • Fix: Ensure route exports
    Route
    constant:
    export const Route = createFileRoute('/path')({ loader: ... })
Issue #5: Memory Leak with TanStack Form (FIXED)
  • Error: Production crashes when using TanStack Form + Router
  • Source: GitHub Issue #5734 (closed Jan 5, 2026)
  • Resolution: Fixed in latest versions of @tanstack/form and @tanstack/react-start. Update both packages to resolve.
Issue #6: Virtual Routes Index/Layout Conflict
  • Error: route.tsx and index.tsx conflict when using
    physical()
    in virtual routing
  • Source: GitHub Issue #5421
  • Fix: Use pathless route instead:
    _layout.tsx
    +
    _layout.index.tsx
Issue #7: Search Params Type Inference
  • Error: Type inference not working with
    zodSearchValidator
  • Source: GitHub Issue #3100 (regression since v1.81.5)
  • Fix: Use
    zodValidator
    from
    @tanstack/zod-adapter
    instead
Issue #8: TanStack Start Validators on Reload
  • Error:
    validateSearch
    not working on page reload in TanStack Start
  • Source: GitHub Issue #3711
  • Note: Works on client-side navigation, fails on direct page load
  • 错误: 构建失败,提示
    @tanstack/router-devtools-core
    未找到
  • 修复: 执行
    npm install @tanstack/router-devtools
问题 #2: Vite插件顺序(关键)
  • 错误: 路由未自动生成,
    routeTree.gen.ts
    缺失
  • 修复: TanStackRouterVite必须在plugins数组中的react()之前
  • 原因: 插件在React编译前处理路由文件
问题 #3: 类型注册缺失
  • 错误:
    <Link to="...">
    无类型支持,无自动补全
  • 修复: 在main.tsx中从
    ./routeTree.gen
    导入
    routeTree
    以注册类型
问题 #4: 加载器未运行
  • 错误: 导航时加载器函数未被调用
  • 修复: 确保路由导出
    Route
    常量:
    export const Route = createFileRoute('/path')({ loader: ... })
问题 #5: TanStack Form内存泄漏(已修复)
  • 错误: 使用TanStack Form + Router时生产环境崩溃
  • 来源: GitHub Issue #5734(2026年1月5日关闭)
  • 解决方案: 更新@tanstack/form和@tanstack/react-start到最新版本
问题 #6: 虚拟路由索引/布局冲突
  • 错误: 在虚拟路由中使用
    physical()
    时,route.tsx和index.tsx冲突
  • 来源: GitHub Issue #5421
  • 修复: 使用无路径路由替代:
    _layout.tsx
    +
    _layout.index.tsx
问题 #7: 搜索参数类型推断
  • 错误: 使用
    zodSearchValidator
    时类型推断失效
  • 来源: GitHub Issue #3100(v1.81.5后出现回归)
  • 修复: 使用
    @tanstack/zod-adapter
    中的
    zodValidator
    替代
问题 #8: TanStack Start验证器在刷新时失效
  • 错误: 在TanStack Start中,页面刷新时
    validateSearch
    失效
  • 来源: GitHub Issue #3711
  • 说明: 客户端导航时正常工作,直接加载页面时失效

Issue #9: Server Function Validation Errors Lose Structure

问题 #9: 服务器函数验证错误丢失结构

Error:
inputValidator
Zod errors stringified, losing structure on client Source: GitHub Issue #6428 Why It Happens: TanStack Start server function error serialization converts Zod issues array to JSON string in
error.message
, making it unusable without manual parsing.
Prevention:
typescript
// Server function with input validation
export const myFn = createServerFn({ method: 'POST' })
  .inputValidator(z.object({
    name: z.string().min(2),
    age: z.number().min(18),
  }))
  .handler(async ({ data }) => data)

// Client: Workaround to parse stringified issues
try {
  await mutation.mutate({ data: invalidData })
} catch (error) {
  if (error.message.startsWith('[')) {
    const issues = JSON.parse(error.message)
    // Now can use structured error data
    issues.forEach(issue => {
      console.log(issue.path, issue.message)
    })
  }
}
Official Status: Known issue, tracking PR for fix
错误:
inputValidator
的Zod错误被序列化,在客户端丢失结构 来源: GitHub Issue #6428 原因: TanStack Start的服务器函数错误序列化将Zod issues数组转换为
error.message
中的JSON字符串,不手动解析无法使用
预防方案:
typescript
// 带输入验证的服务器函数
export const myFn = createServerFn({ method: 'POST' })
  .inputValidator(z.object({
    name: z.string().min(2),
    age: z.number().min(18),
  }))
  .handler(async ({ data }) => data)

// 客户端:解析字符串化错误的临时方案
try {
  await mutation.mutate({ data: invalidData })
} catch (error) {
  if (error.message.startsWith('[')) {
    const issues = JSON.parse(error.message)
    // 现在可以使用结构化错误数据
    issues.forEach(issue => {
      console.log(issue.path, issue.message)
    })
  }
}
官方状态: 已知问题,正在跟踪修复PR

Issue #10: useParams({ strict: false }) Returns Unparsed Values

问题 #10: useParams({ strict: false })返回未解析的值

Error: Params typed as parsed but returned as strings after navigation Source: GitHub Issue #6385 Why It Happens: In v1.147.3+,
match.params
is no longer parsed when using
strict: false
. First render works correctly, but after navigation values are stored as strings instead of parsed types.
Prevention:
typescript
// Route with param parsing
export const Route = createFileRoute('/posts/$postId')({
  params: {
    parse: (params) => ({
      postId: z.coerce.number().parse(params.postId),
    }),
  },
})

// Component: Use strict mode (default) for parsed params
function Component() {
  const { postId } = useParams() // ✓ Parsed as number
  // const { postId } = useParams({ strict: false }) // ✗ String!

  // Or manually parse when using strict: false
  const params = useParams({ strict: false })
  const postId = Number(params.postId)
}
Official Status: Known issue, workaround required
错误: 参数被标记为已解析类型,但导航后返回字符串 来源: GitHub Issue #6385 原因: 在v1.147.3+版本中,使用
strict: false
match.params
不再被解析。首次渲染正常,但导航后值以字符串而非解析类型存储
预防方案:
typescript
// 带参数解析的路由
export const Route = createFileRoute('/posts/$postId')({
  params: {
    parse: (params) => ({
      postId: z.coerce.number().parse(params.postId),
    }),
  },
})

// 组件:使用严格模式(默认)获取已解析参数
function Component() {
  const { postId } = useParams() // ✓ 已解析为数字
  // const { postId } = useParams({ strict: false }) // ✗ 返回字符串!

  // 或者在使用strict: false时手动解析
  const params = useParams({ strict: false })
  const postId = Number(params.postId)
}
官方状态: 已知问题,需使用临时方案

Issue #11: Pathless Route notFoundComponent Not Rendering

问题 #11: 无路径路由的notFoundComponent不渲染

Error:
notFoundComponent
on pathless layout routes ignored Source: GitHub Issue #6351, GitHub Issue #4065 Why It Happens: Pathless routes (e.g.,
routes/(authenticated)/route.tsx
) don't render their
notFoundComponent
. Instead, the
defaultNotFoundComponent
from router config is triggered. This has been broken since April 2025.
Prevention:
typescript
// ✗ Doesn't work: notFoundComponent on pathless layout
export const Route = createFileRoute('/(authenticated)')({
  beforeLoad: ({ context }) => {
    if (!context.auth) throw redirect({ to: '/login' })
  },
  notFoundComponent: () => <div>Protected 404</div>, // Not rendered!
})

// ✓ Works: Define on child routes instead
export const Route = createFileRoute('/(authenticated)/dashboard')({
  notFoundComponent: () => <div>Protected 404</div>,
})
Official Status: Known issue, workaround required
错误: 无路径布局路由上的
notFoundComponent
被忽略 来源: GitHub Issue #6351, GitHub Issue #4065 原因: 无路径路由(如
routes/(authenticated)/route.tsx
)不会渲染其
notFoundComponent
,而是触发路由配置中的
defaultNotFoundComponent
。该问题自2025年4月起存在
预防方案:
typescript
// ✗ 无效:在无路径布局上设置notFoundComponent
export const Route = createFileRoute('/(authenticated)')({
  beforeLoad: ({ context }) => {
    if (!context.auth) throw redirect({ to: '/login' })
  },
  notFoundComponent: () => <div>受保护页面404</div>, // 不会渲染!
})

// ✓ 有效:在子路由上定义
export const Route = createFileRoute('/(authenticated)/dashboard')({
  notFoundComponent: () => <div>受保护页面404</div>,
})
官方状态: 已知问题,需使用临时方案

Issue #12: Aborted Loader Renders errorComponent with Undefined Error

问题 #12: 已中止的加载器渲染errorComponent但传递undefined错误

Error: Rapid navigation aborts previous loader and renders errorComponent with
undefined
error Source: GitHub Issue #6388 Why It Happens: Side effect introduced after PR #4570. When user rapidly navigates (e.g., clicking through list items), aborted fetch requests trigger errorComponent without passing the abort error.
Prevention:
typescript
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params, abortController }) => {
    await fetch(`/api/posts/${params.postId}`, {
      signal: abortController.signal,
    })
  },
  errorComponent: ({ error, reset }) => {
    // Check for undefined error (aborted request)
    if (!error) {
      return null // Or show loading state
    }
    return <div>Error: {error.message}</div>
  },
})
Official Status: Known issue, workaround required
错误: 快速导航中止之前的加载器,渲染errorComponent但未传递错误 来源: GitHub Issue #6388 原因: PR #4570引入的副作用。当用户快速导航(如点击列表项)时,已中止的请求触发errorComponent但未传递中止错误
预防方案:
typescript
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params, abortController }) => {
    await fetch(`/api/posts/${params.postId}`, {
      signal: abortController.signal,
    })
  },
  errorComponent: ({ error, reset }) => {
    // 检查undefined错误(已中止的请求)
    if (!error) {
      return null // 或展示加载状态
    }
    return <div>错误: {error.message}</div>
  },
})
官方状态: 已知问题,需使用临时方案

Issue #13: Vitest Cannot Read Properties of Null (useState)

问题 #13: Vitest无法读取null的属性(useState)

Error:
Cannot read properties of null (reading 'useState')
when running tests with Vitest Source: GitHub Issue #6262, PR #6074 Why It Happens: TanStack Start's
tanstackStart()
plugin conflicts with Vitest's React hooks rendering. This is a known duplicate issue with a PR in progress.
Prevention:
typescript
// Temporary workaround: Comment out tanstackStart() for tests
// vite.config.ts
export default defineConfig({
  plugins: [
    // tanstackStart(), // Disable for tests
    react(),
  ],
  test: { environment: 'jsdom' },
})
Official Status: PR #6074 in progress to fix
错误: 使用Vitest运行测试时出现
Cannot read properties of null (reading 'useState')
来源: GitHub Issue #6262, PR #6074 原因: TanStack Start的
tanstackStart()
插件与Vitest的React钩子渲染冲突。这是已知重复问题,修复PR正在进行中
预防方案:
typescript
// 临时方案:测试时注释掉tanstackStart()
// vite.config.ts
export default defineConfig({
  plugins: [
    // tanstackStart(), // 测试时禁用
    react(),
  ],
  test: { environment: 'jsdom' },
})
官方状态: 修复PR #6074正在进行中

Issue #14: Throwing Error in Streaming SSR Loader Crashes Dev Server

问题 #14: 在流式SSR加载器中抛出错误导致开发服务器崩溃

Error: Dev server crashes when route loader throws error without awaiting (using
void
instead of
await
) Source: GitHub Issue #6200 Why It Happens: SSR streaming mode can't handle unawaited promise rejections. The error escapes the loader context and crashes the worker process.
Prevention:
typescript
// ✗ Wrong: void + throw crashes dev server
export const Route = createFileRoute('/posts')({
  loader: async () => {
    void fetch('/api/posts').then(r => {
      throw new Error('boom') // Crashes!
    })
  },
})

// ✓ Correct: Always await or catch
export const Route = createFileRoute('/posts')({
  loader: async () => {
    try {
      const data = await fetch('/api/posts')
      return data
    } catch (error) {
      throw error // Caught by errorComponent
    }
  },
})
Official Status: Known issue, workaround required
错误: 路由加载器在未等待的情况下抛出错误(使用
void
而非
await
)时开发服务器崩溃 来源: GitHub Issue #6200 原因: SSR流式模式无法处理未等待的Promise拒绝。错误逃离加载器上下文并导致工作进程崩溃
预防方案:
typescript
// ✗ 错误用法:void + throw导致开发服务器崩溃
export const Route = createFileRoute('/posts')({
  loader: async () => {
    void fetch('/api/posts').then(r => {
      throw new Error('boom') // 崩溃!
    })
  },
})

// ✓ 正确用法:始终使用await或捕获错误
export const Route = createFileRoute('/posts')({
  loader: async () => {
    try {
      const data = await fetch('/api/posts')
      return data
    } catch (error) {
      throw error // 被errorComponent捕获
    }
  },
})
官方状态: 已知问题,需使用临时方案

Issue #15: Prerender Hangs Indefinitely if Filter Returns Zero Results

问题 #15: 预渲染时如果过滤器返回零结果会无限挂起

Error: Build step hangs when
prerender.filter
returns zero routes Source: GitHub Issue #6425 Why It Happens: TanStack Start prerendering doesn't handle empty route sets gracefully - it waits indefinitely for routes that never come.
Prevention:
typescript
// ✗ Wrong: Empty filter causes hang
tanstackStart({
  prerender: {
    enabled: true,
    filter: (route) => false, // No routes → hangs!
  },
})

// ✓ Correct: Ensure at least one route or disable
tanstackStart({
  prerender: {
    enabled: true,
    filter: (route) => route.path === '/' || route.path.startsWith('/posts'),
  },
})

// Or temporarily disable
tanstackStart({
  prerender: { enabled: false },
})
Official Status: Known issue, workaround required
错误: 当
prerender.filter
返回零路由时构建步骤无限挂起 来源: GitHub Issue #6425 原因: TanStack Start预渲染无法优雅处理空路由集 - 会无限等待永远不会出现的路由
预防方案:
typescript
// ✗ 错误用法:空过滤器导致挂起
tanstackStart({
  prerender: {
    enabled: true,
    filter: (route) => false, // 无路由 → 挂起!
  },
})

// ✓ 正确用法:确保至少有一个路由或禁用预渲染
tanstackStart({
  prerender: {
    enabled: true,
    filter: (route) => route.path === '/' || route.path.startsWith('/posts'),
  },
})

// 或临时禁用
tanstackStart({
  prerender: { enabled: false },
})
官方状态: 已知问题,需使用临时方案

Issue #16: Prerendering Does Not Work in Docker

问题 #16: Docker中预渲染无法工作

Error: Build fails in Docker with "Unable to connect" during prerender step Source: GitHub Issue #6275, PR #6305 Why It Happens: Vite preview server used for prerendering is not accessible in Docker environment.
Prevention:
typescript
// vite.config.ts - Make preview server accessible in Docker
export default defineConfig({
  preview: {
    host: true, // Bind to 0.0.0.0 instead of localhost
  },
  plugins: [
    devtools(),
    // nitro({ preset: "bun" }), // Remove temporarily if issues persist
    tanstackStart(),
    react(),
  ],
})
Official Status: PR #6305 in progress
错误: Docker中构建失败,预渲染步骤出现"无法连接" 来源: GitHub Issue #6275, PR #6305 原因: 预渲染使用的Vite预览服务器在Docker环境中无法访问
预防方案:
typescript
// vite.config.ts - 让预览服务器在Docker中可访问
export default defineConfig({
  preview: {
    host: true, // 绑定到0.0.0.0而非localhost
  },
  plugins: [
    devtools(),
    // nitro({ preset: "bun" }), // 如有问题可临时移除
    tanstackStart(),
    react(),
  ],
})
官方状态: 修复PR #6305正在进行中

Issue #17: Route Head Function Executes Before Loader Finishes

问题 #17: 路由head函数在加载器完成前执行

Error: Meta tags generated with incomplete data when
head()
runs before
loader()
Source: GitHub Issue #6221 Why It Happens: The
head()
function can execute before the route
loader()
finishes, causing meta tags to use placeholder or undefined data.
Prevention:
typescript
// ✗ Wrong: loaderData may not be available yet
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    return { post }
  },
  head: ({ loaderData }) => ({
    meta: [
      { title: loaderData.post.title }, // May be undefined!
    ],
  }),
})

// ✓ Correct: Explicitly await if needed
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    return { post }
  },
  head: async ({ loaderData }) => {
    await loaderData // Ensure loaded
    return {
      meta: [{ title: loaderData.post.title }],
    }
  },
})
Official Status: Known issue, workaround required
错误: 当
head()
loader()
完成前运行时,元标签使用不完整数据生成 来源: GitHub Issue #6221 原因: 路由
head()
函数可能在
loader()
完成前执行,导致元标签使用占位符或未定义数据
预防方案:
typescript
// ✗ 错误用法:loaderData可能尚未可用
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    return { post }
  },
  head: ({ loaderData }) => ({
    meta: [
      { title: loaderData.post.title }, // 可能未定义!
    ],
  }),
})

// ✓ 正确用法:必要时显式等待
export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params }) => {
    const post = await fetchPost(params.postId)
    return { post }
  },
  head: async ({ loaderData }) => {
    await loaderData // 确保数据已加载
    return {
      meta: [{ title: loaderData.post.title }],
    }
  },
})
官方状态: 已知问题,需使用临时方案

Issue #18: Virtual Routes Don't Support Manual Lazy Loading (Community-sourced)

问题 #18: 虚拟路由不支持手动懒加载(社区反馈)

Error:
createLazyFileRoute
automatically replaced with
createFileRoute
in virtual routes Source: GitHub Issue #6396 Why It Happens: Virtual file routes are designed for automatic code splitting only. Manual lazy routes are not supported - the plugin silently replaces them.
Prevention:
typescript
// Virtual routes: Use automatic code splitting
// vite.config.ts
tanstackRouter({
  target: 'react',
  virtualRouteConfig: './routes.ts',
  autoCodeSplitting: true, // Use automatic splitting
})

// Don't use createLazyFileRoute in virtual routes
// It will be replaced with createFileRoute automatically
Official Status: By design (documented behavior)
错误: 虚拟路由中的
createLazyFileRoute
被自动替换为
createFileRoute
来源: GitHub Issue #6396 原因: 虚拟文件路由仅设计用于自动代码分割。不支持手动懒加载 - 插件会静默替换
预防方案:
typescript
// 虚拟路由:使用自动代码分割
// vite.config.ts
tanstackRouter({
  target: 'react',
  virtualRouteConfig: './routes.ts',
  autoCodeSplitting: true, // 使用自动分割
})

// 不要在虚拟路由中使用createLazyFileRoute
// 它会被自动替换为createFileRoute
官方状态: 设计如此(文档已说明)

Issue #19: NavigateOptions Type Safety Inconsistency (Community-sourced)

问题 #19: NavigateOptions类型安全不一致(社区反馈)

Error:
NavigateOptions
type doesn't enforce required params like
useNavigate()
does Source: TkDodo's Blog: The Beauty of TanStack Router Why It Happens: Type definitions differ between runtime hook and type helper.
NavigateOptions
is less strict.
Prevention:
typescript
// ✗ Wrong: NavigateOptions doesn't catch missing params
const options: NavigateOptions = {
  to: '/posts/$postId', // No TS error, but params required!
}

// ✓ Correct: Use useNavigate() return type
const navigate = useNavigate()
type NavigateFn = typeof navigate
// Now type-safe across all usages
Verified: Cross-referenced with TanStack Query maintainer analysis
错误:
NavigateOptions
类型不像
useNavigate()
那样强制要求必填参数 来源: TkDodo博客: The Beauty of TanStack Router 原因: 运行时钩子和类型助手的类型定义不同。
NavigateOptions
的严格性更低
预防方案:
typescript
// ✗ 错误用法:NavigateOptions无法捕获缺失的参数
const options: NavigateOptions = {
  to: '/posts/$postId', // 无TS错误,但参数是必填的!
}

// ✓ 正确用法:使用useNavigate()的返回类型
const navigate = useNavigate()
type NavigateFn = typeof navigate
// 现在所有用法都类型安全
验证: 已与TanStack Query维护者分析交叉验证

Issue #20: Missing Leading Slash in Route Paths (Community-sourced)

问题 #20: 路由路径缺少前导斜杠(社区反馈)

Error: Routes fail to match when path defined without leading slash Source: Official Debugging Guide Why It Happens: Very common beginner mistake - using
'about'
instead of
'/about'
causes route matching failures.
Prevention:
typescript
// ✗ Wrong: Missing leading slash
export const Route = createFileRoute('about')({ /* ... */ })

// ✓ Correct: Always start with /
export const Route = createFileRoute('/about')({ /* ... */ })
Verified: Official documentation, common debugging issue

错误: 路径定义时缺少前导斜杠导致路由匹配失败 来源: 官方调试指南 原因: 非常常见的新手错误 - 使用
'about'
而非
'/about'
会导致路由匹配失败
预防方案:
typescript
// ✗ 错误用法:缺少前导斜杠
export const Route = createFileRoute('about')({ /* ... */ })

// ✓ 正确用法:始终以/开头
export const Route = createFileRoute('/about')({ /* ... */ })
验证: 官方文档已说明,常见调试问题

Cloudflare Workers Integration

Cloudflare Workers集成

Vite Config (add @cloudflare/vite-plugin):
typescript
import { cloudflare } from '@cloudflare/vite-plugin'

export default defineConfig({
  plugins: [TanStackRouterVite(), react(), cloudflare()],
})
API Routes Pattern (fetch from Workers backend):
typescript
// Worker: functions/api/posts.ts
export async function onRequestGet({ env }) {
  const { results } = await env.DB.prepare('SELECT * FROM posts').all()
  return Response.json(results)
}

// Router: src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: async () => fetch('/api/posts').then(r => r.json()),
})

Related Skills: tanstack-query (data fetching), react-hook-form-zod (form validation), cloudflare-worker-base (API backend), tailwind-v4-shadcn (UI)
Related Packages: @tanstack/zod-adapter (search validation), @tanstack/virtual-file-routes (programmatic routes)

Last verified: 2026-01-20 | Skill version: 2.0.0 | Changes: Added 12 new issues from community research (inputValidator structure loss, useParams parsing bug, pathless notFoundComponent, aborted loader errors, Vitest conflicts, SSR streaming crashes, Docker prerender issues, head/loader timing, virtual routes lazy loading limitation, NavigateOptions type inconsistency, leading slash common mistake). Increased error prevention from 8 to 20 documented issues.
Vite配置(添加@cloudflare/vite-plugin):
typescript
import { cloudflare } from '@cloudflare/vite-plugin'

export default defineConfig({
  plugins: [TanStackRouterVite(), react(), cloudflare()],
})
API路由模式(从Workers后端获取数据):
typescript
// Worker: functions/api/posts.ts
export async function onRequestGet({ env }) {
  const { results } = await env.DB.prepare('SELECT * FROM posts').all()
  return Response.json(results)
}

// Router: src/routes/posts.tsx
export const Route = createFileRoute('/posts')({
  loader: async () => fetch('/api/posts').then(r => r.json()),
})

相关技能: tanstack-query(数据获取), react-hook-form-zod(表单验证), cloudflare-worker-base(API后端), tailwind-v4-shadcn(UI)
相关包: @tanstack/zod-adapter(搜索验证), @tanstack/virtual-file-routes(编程式路由)

最后验证: 2026-01-20 | 技能版本: 2.0.0 | 变更: 新增12个来自社区研究的问题(inputValidator结构丢失、useParams解析错误、无路径路由notFoundComponent问题、已中止加载器错误、Vitest冲突、SSR流崩溃、Docker预渲染问题、head/loader时序问题、虚拟路由懒加载限制、NavigateOptions类型不一致、前导斜杠常见错误)。将可预防的错误数量从8种增加到20种。