tanstack-router
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOverview
概述
TanStack Router is a fully type-safe router for React (and Solid) applications. It provides file-based routing, first-class search parameter management, built-in data loading, code splitting, and deep TypeScript integration. It serves as the routing foundation for TanStack Start (the full-stack framework).
Package:
CLI: or (Vite/Rspack/Webpack)
Devtools:
@tanstack/react-router@tanstack/router-cli@tanstack/router-plugin@tanstack/react-router-devtoolsTanStack Router 是一款为React(及Solid)应用打造的全类型安全路由库。它支持基于文件的路由、一流的搜索参数管理、内置数据加载、代码分割以及深度TypeScript集成,是TanStack Start(全栈框架)的路由基础。
Package:
CLI: 或 (Vite/Rspack/Webpack)
Devtools:
@tanstack/react-router@tanstack/router-cli@tanstack/router-plugin@tanstack/react-router-devtoolsInstallation
安装
bash
npm install @tanstack/react-routerbash
npm install @tanstack/react-routerFor file-based routing with Vite:
For file-based routing with Vite:
npm install -D @tanstack/router-plugin
npm install -D @tanstack/router-plugin
Or standalone CLI:
Or standalone CLI:
npm install -D @tanstack/router-cli
undefinednpm install -D @tanstack/router-cli
undefinedCore Concepts
核心概念
Route Trees
路由树
Routes are organized in a tree structure. The root route is the top-level layout, and child routes nest underneath.
tsx
import { createRootRoute, createRoute, createRouter } from '@tanstack/react-router'
const rootRoute = createRootRoute({
component: RootLayout,
})
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: HomePage,
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
component: AboutPage,
})
const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])
const router = createRouter({ routeTree })路由以树形结构组织。根路由是顶级布局,子路由嵌套在其下。
tsx
import { createRootRoute, createRoute, createRouter } from '@tanstack/react-router'
const rootRoute = createRootRoute({
component: RootLayout,
})
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: HomePage,
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/about',
component: AboutPage,
})
const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])
const router = createRouter({ routeTree })File-Based Routing
基于文件的路由
File-based routing automatically generates the route tree from your file structure. Configure with Vite plugin:
ts
// vite.config.ts
import { defineConfig } from 'vite'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
TanStackRouterVite(),
// ... other plugins
],
})基于文件的路由会根据你的文件结构自动生成路由树。通过Vite插件进行配置:
ts
// vite.config.ts
import { defineConfig } from 'vite'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
TanStackRouterVite(),
// ... other plugins
],
})File Naming Conventions
文件命名规范
| File Pattern | Route Type | Example Path |
|---|---|---|
| Root layout | N/A (wraps all) |
| Index route | |
| Static route | |
| Dynamic param | |
| Layout route | |
| Nested index | |
| Nested dynamic | |
| Pathless layout | |
| Pathless layout | N/A (groups routes) |
| Grouped route | |
| Splat/catch-all | |
| Dot notation | |
| 文件模式 | 路由类型 | 示例路径 |
|---|---|---|
| 根布局 | N/A(包裹所有路由) |
| 索引路由 | |
| 静态路由 | |
| 动态参数路由 | |
| 布局路由 | |
| 嵌套索引路由 | |
| 嵌套动态路由 | |
| 无路径布局路由 | |
| 无路径布局路由 | N/A(分组路由) |
| 分组路由 | |
| 通配/捕获所有路由 | |
| 点符号路由 | |
Special Prefixes
特殊前缀
- prefix: Pathless routes (layout groups without URL segment)
_ - prefix: Dynamic path parameters
$ - parentheses: Route groups (organizational, no URL impact)
(folder)
- 前缀:无路径路由(无URL片段的布局分组)
_ - 前缀:动态路径参数
$ - 括号:路由分组(仅用于组织,不影响URL)
(folder)
Route Configuration
路由配置
Each route can define:
tsx
// routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// Validation for path params
params: {
parse: (params) => ({ postId: Number(params.postId) }),
stringify: (params) => ({ postId: String(params.postId) }),
},
// Search params validation
validateSearch: (search: Record<string, unknown>) => {
return {
page: Number(search.page ?? 1),
filter: (search.filter as string) || '',
}
},
// Data loading
loader: async ({ params, context, abortController }) => {
return fetchPost(params.postId)
},
// Loader dependencies (re-run loader when these change)
loaderDeps: ({ search }) => ({ page: search.page }),
// Stale time for cached loader data
staleTime: 5_000,
// Preloading
preloadStaleTime: 30_000,
// Error component
errorComponent: PostErrorComponent,
// Pending/loading component
pendingComponent: PostLoadingComponent,
// 404 component
notFoundComponent: PostNotFoundComponent,
// Before load hook (authentication, redirects)
beforeLoad: async ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
},
// Head/meta management
head: () => ({
meta: [{ title: 'Post Details' }],
}),
// Component
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
const post = Route.useLoaderData()
const { page, filter } = Route.useSearch()
return <div>{post.title}</div>
}每个路由可以定义:
tsx
// routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// Validation for path params
params: {
parse: (params) => ({ postId: Number(params.postId) }),
stringify: (params) => ({ postId: String(params.postId) }),
},
// Search params validation
validateSearch: (search: Record<string, unknown>) => {
return {
page: Number(search.page ?? 1),
filter: (search.filter as string) || '',
}
},
// Data loading
loader: async ({ params, context, abortController }) => {
return fetchPost(params.postId)
},
// Loader dependencies (re-run loader when these change)
loaderDeps: ({ search }) => ({ page: search.page }),
// Stale time for cached loader data
staleTime: 5_000,
// Preloading
preloadStaleTime: 30_000,
// Error component
errorComponent: PostErrorComponent,
// Pending/loading component
pendingComponent: PostLoadingComponent,
// 404 component
notFoundComponent: PostNotFoundComponent,
// Before load hook (authentication, redirects)
beforeLoad: async ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
},
// Head/meta management
head: () => ({
meta: [{ title: 'Post Details' }],
}),
// Component
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
const post = Route.useLoaderData()
const { page, filter } = Route.useSearch()
return <div>{post.title}</div>
}Data Loading
数据加载
Route Loaders
路由Loader
tsx
export const Route = createFileRoute('/posts')({
loader: async ({ context }) => {
// Access router context (e.g., queryClient)
const posts = await context.queryClient.ensureQueryData({
queryKey: ['posts'],
queryFn: fetchPosts,
})
return { posts }
},
component: PostsComponent,
})
function PostsComponent() {
const { posts } = Route.useLoaderData()
// ...
}tsx
export const Route = createFileRoute('/posts')({
loader: async ({ context }) => {
// Access router context (e.g., queryClient)
const posts = await context.queryClient.ensureQueryData({
queryKey: ['posts'],
queryFn: fetchPosts,
})
return { posts }
},
component: PostsComponent,
})
function PostsComponent() {
const { posts } = Route.useLoaderData()
// ...
}Loader Dependencies
Loader依赖
Control when loaders re-execute:
tsx
export const Route = createFileRoute('/posts')({
loaderDeps: ({ search: { page, filter } }) => ({ page, filter }),
loader: async ({ deps: { page, filter } }) => {
return fetchPosts({ page, filter })
},
})控制Loader重新执行的时机:
tsx
export const Route = createFileRoute('/posts')({
loaderDeps: ({ search: { page, filter } }) => ({ page, filter }),
loader: async ({ deps: { page, filter } }) => {
return fetchPosts({ page, filter })
},
})Deferred Data Loading
延迟数据加载
Stream non-critical data:
tsx
import { Await, defer } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
loader: async () => {
const criticalData = await fetchCriticalData()
const deferredData = defer(fetchSlowData())
return { criticalData, deferredData }
},
component: DashboardComponent,
})
function DashboardComponent() {
const { criticalData, deferredData } = Route.useLoaderData()
return (
<div>
<CriticalSection data={criticalData} />
<Suspense fallback={<Loading />}>
<Await promise={deferredData}>
{(data) => <SlowSection data={data} />}
</Await>
</Suspense>
</div>
)
}流式加载非关键数据:
tsx
import { Await, defer } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
loader: async () => {
const criticalData = await fetchCriticalData()
const deferredData = defer(fetchSlowData())
return { criticalData, deferredData }
},
component: DashboardComponent,
})
function DashboardComponent() {
const { criticalData, deferredData } = Route.useLoaderData()
return (
<div>
<CriticalSection data={criticalData} />
<Suspense fallback={<Loading />}>
<Await promise={deferredData}>
{(data) => <SlowSection data={data} />}
</Await>
</Suspense>
</div>
)
}Context-Based Data Loading
基于上下文的数据加载
Provide shared dependencies via router context:
tsx
// Create router with context
const router = createRouter({
routeTree,
context: {
queryClient,
auth: undefined!, // Will be provided by RouterProvider
},
})
// In root/app component
function App() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
// In routes
export const Route = createFileRoute('/protected')({
beforeLoad: ({ context }) => {
if (!context.auth.user) throw redirect({ to: '/login' })
},
loader: ({ context }) => {
return context.queryClient.ensureQueryData(userQueryOptions())
},
})通过路由上下文提供共享依赖:
tsx
// Create router with context
const router = createRouter({
routeTree,
context: {
queryClient,
auth: undefined!, // Will be provided by RouterProvider
},
})
// In root/app component
function App() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
// In routes
export const Route = createFileRoute('/protected')({
beforeLoad: ({ context }) => {
if (!context.auth.user) throw redirect({ to: '/login' })
},
loader: ({ context }) => {
return context.queryClient.ensureQueryData(userQueryOptions())
},
})Search Parameters
搜索参数
Validation
验证
tsx
import { z } from 'zod'
const postSearchSchema = z.object({
page: z.number().default(1),
filter: z.string().default(''),
sort: z.enum(['date', 'title']).default('date'),
})
export const Route = createFileRoute('/posts')({
validateSearch: postSearchSchema,
// Or manual validation:
// validateSearch: (search) => postSearchSchema.parse(search),
})tsx
import { z } from 'zod'
const postSearchSchema = z.object({
page: z.number().default(1),
filter: z.string().default(''),
sort: z.enum(['date', 'title']).default('date'),
})
export const Route = createFileRoute('/posts')({
validateSearch: postSearchSchema,
// Or manual validation:
// validateSearch: (search) => postSearchSchema.parse(search),
})Reading Search Params
读取搜索参数
tsx
function PostsComponent() {
// From route
const { page, filter, sort } = Route.useSearch()
// Or from any component with useSearch hook
const search = useSearch({ from: '/posts' })
}tsx
function PostsComponent() {
// From route
const { page, filter, sort } = Route.useSearch()
// Or from any component with useSearch hook
const search = useSearch({ from: '/posts' })
}Updating Search Params
更新搜索参数
tsx
import { useNavigate } from '@tanstack/react-router'
function Pagination() {
const navigate = useNavigate()
const { page } = Route.useSearch()
return (
<button
onClick={() =>
navigate({
search: (prev) => ({ ...prev, page: prev.page + 1 }),
})
}
>
Next Page
</button>
)
}
// Or via Link component
<Link
to="/posts"
search={(prev) => ({ ...prev, page: 2 })}
>
Page 2
</Link>tsx
import { useNavigate } from '@tanstack/react-router'
function Pagination() {
const navigate = useNavigate()
const { page } = Route.useSearch()
return (
<button
onClick={() =>
navigate({
search: (prev) => ({ ...prev, page: prev.page + 1 }),
})
}
>
Next Page
</button>
)
}
// Or via Link component
<Link
to="/posts"
search={(prev) => ({ ...prev, page: 2 })}
>
Page 2
</Link>Search Param Options
搜索参数选项
tsx
const router = createRouter({
routeTree,
// Custom serialization
search: {
strict: true, // Reject unknown params
},
// Default search param serializer
stringifySearch: defaultStringifySearch,
parseSearch: defaultParseSearch,
})tsx
const router = createRouter({
routeTree,
// Custom serialization
search: {
strict: true, // Reject unknown params
},
// Default search param serializer
stringifySearch: defaultStringifySearch,
parseSearch: defaultParseSearch,
})Navigation
导航
Link Component
Link组件
tsx
import { Link } from '@tanstack/react-router'
// Static route
<Link to="/about">About</Link>
// Dynamic route with params
<Link to="/posts/$postId" params={{ postId: '123' }}>
Post 123
</Link>
// With search params
<Link to="/posts" search={{ page: 2, filter: 'react' }}>
Page 2
</Link>
// Active link styling
<Link
to="/posts"
activeProps={{ className: 'active' }}
inactiveProps={{ className: 'inactive' }}
activeOptions={{ exact: true }}
>
Posts
</Link>
// Preloading
<Link to="/posts" preload="intent">Posts</Link>
<Link to="/dashboard" preload="viewport">Dashboard</Link>
// Hash
<Link to="/docs" hash="api-reference">API Reference</Link>tsx
import { Link } from '@tanstack/react-router'
// Static route
<Link to="/about">About</Link>
// Dynamic route with params
<Link to="/posts/$postId" params={{ postId: '123' }}>
Post 123
</Link>
// With search params
<Link to="/posts" search={{ page: 2, filter: 'react' }}>
Page 2
</Link>
// Active link styling
<Link
to="/posts"
activeProps={{ className: 'active' }}
inactiveProps={{ className: 'inactive' }}
activeOptions={{ exact: true }}
>
Posts
</Link>
// Preloading
<Link to="/posts" preload="intent">Posts</Link>
<Link to="/dashboard" preload="viewport">Dashboard</Link>
// Hash
<Link to="/docs" hash="api-reference">API Reference</Link>Programmatic Navigation
编程式导航
tsx
import { useNavigate, useRouter } from '@tanstack/react-router'
function MyComponent() {
const navigate = useNavigate()
const router = useRouter()
// Navigate to a route
navigate({ to: '/posts', search: { page: 1 } })
// Navigate with replace
navigate({ to: '/posts', replace: true })
// Relative navigation
navigate({ to: '.', search: (prev) => ({ ...prev, page: 2 }) })
// Go back/forward
router.history.back()
router.history.forward()
// Invalidate and reload current route
router.invalidate()
}tsx
import { useNavigate, useRouter } from '@tanstack/react-router'
function MyComponent() {
const navigate = useNavigate()
const router = useRouter()
// Navigate to a route
navigate({ to: '/posts', search: { page: 1 } })
// Navigate with replace
navigate({ to: '/posts', replace: true })
// Relative navigation
navigate({ to: '.', search: (prev) => ({ ...prev, page: 2 }) })
// Go back/forward
router.history.back()
router.history.forward()
// Invalidate and reload current route
router.invalidate()
}Redirects
重定向
tsx
import { redirect } from '@tanstack/react-router'
// In beforeLoad or loader
throw redirect({
to: '/login',
search: { redirect: location.href },
// Optional status code
statusCode: 301, // Permanent redirect (SSR)
})tsx
import { redirect } from '@tanstack/react-router'
// In beforeLoad or loader
throw redirect({
to: '/login',
search: { redirect: location.href },
// Optional status code
statusCode: 301, // Permanent redirect (SSR)
})Navigation Blocking
导航拦截
tsx
import { useBlocker } from '@tanstack/react-router'
function FormComponent() {
const [isDirty, setIsDirty] = useState(false)
useBlocker({
shouldBlockFn: () => isDirty,
withResolver: true, // Shows confirm dialog
})
// Or with custom UI
const { proceed, reset, status } = useBlocker({
shouldBlockFn: () => isDirty,
})
if (status === 'blocked') {
return (
<div>
<p>Are you sure you want to leave?</p>
<button onClick={proceed}>Leave</button>
<button onClick={reset}>Stay</button>
</div>
)
}
}tsx
import { useBlocker } from '@tanstack/react-router'
function FormComponent() {
const [isDirty, setIsDirty] = useState(false)
useBlocker({
shouldBlockFn: () => isDirty,
withResolver: true, // Shows confirm dialog
})
// Or with custom UI
const { proceed, reset, status } = useBlocker({
shouldBlockFn: () => isDirty,
})
if (status === 'blocked') {
return (
<div>
<p>Are you sure you want to leave?</p>
<button onClick={proceed}>Leave</button>
<button onClick={reset}>Stay</button>
</div>
)
}
}Code Splitting
代码分割
Automatic (File-Based Routing)
自动分割(基于文件的路由)
With file-based routing, create a lazy file:
routes/
posts.tsx # Critical: loader, beforeLoad, meta
posts.lazy.tsx # Lazy: component, pendingComponent, errorComponenttsx
// posts.tsx (loaded eagerly)
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
})
// posts.lazy.tsx (loaded lazily)
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/posts')({
component: PostsComponent,
pendingComponent: PostsLoading,
errorComponent: PostsError,
})在基于文件的路由中,创建一个延迟加载文件:
routes/
posts.tsx # Critical: loader, beforeLoad, meta
posts.lazy.tsx # Lazy: component, pendingComponent, errorComponenttsx
// posts.tsx (loaded eagerly)
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
})
// posts.lazy.tsx (loaded lazily)
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/posts')({
component: PostsComponent,
pendingComponent: PostsLoading,
errorComponent: PostsError,
})Manual Code Splitting
手动代码分割
tsx
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
loader: () => fetchPosts(),
}).lazy(() => import('./posts.lazy').then((d) => d.Route))tsx
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
loader: () => fetchPosts(),
}).lazy(() => import('./posts.lazy').then((d) => d.Route))Preloading
预加载
tsx
// Router-level defaults
const router = createRouter({
routeTree,
defaultPreload: 'intent', // 'intent' | 'viewport' | 'render' | false
defaultPreloadStaleTime: 30_000, // 30 seconds
})
// Route-level
export const Route = createFileRoute('/posts/$postId')({
// Stale time for the loader data
staleTime: 5_000,
// How long preloaded data stays fresh
preloadStaleTime: 30_000,
})
// Link-level
<Link to="/posts" preload="intent" preloadDelay={100}>
Posts
</Link>tsx
// Router-level defaults
const router = createRouter({
routeTree,
defaultPreload: 'intent', // 'intent' | 'viewport' | 'render' | false
defaultPreloadStaleTime: 30_000, // 30 seconds
})
// Route-level
export const Route = createFileRoute('/posts/$postId')({
// Stale time for the loader data
staleTime: 5_000,
// How long preloaded data stays fresh
preloadStaleTime: 30_000,
})
// Link-level
<Link to="/posts" preload="intent" preloadDelay={100}>
Posts
</Link>Type Safety
类型安全
Register Router Type
注册路由类型
tsx
// Declare module for type inference
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}tsx
// Declare module for type inference
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}Type-Safe Hooks
类型安全的Hooks
All hooks are fully typed based on the route tree:
tsx
// useParams - typed to route's params
const { postId } = useParams({ from: '/posts/$postId' })
// useSearch - typed to route's search schema
const { page } = useSearch({ from: '/posts' })
// useLoaderData - typed to loader return
const data = useLoaderData({ from: '/posts/$postId' })
// useRouteContext - typed to route context
const { auth } = useRouteContext({ from: '/protected' })所有Hooks都会根据路由树进行全类型推导:
tsx
// useParams - typed to route's params
const { postId } = useParams({ from: '/posts/$postId' })
// useSearch - typed to route's search schema
const { page } = useSearch({ from: '/posts' })
// useLoaderData - typed to loader return
const data = useLoaderData({ from: '/posts/$postId' })
// useRouteContext - typed to route context
const { auth } = useRouteContext({ from: '/protected' })Route Generics
路由泛型
tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// TypeScript infers:
// params: { postId: string }
// search: validated search schema type
// loaderData: return type of loader
// context: router context type
})tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// TypeScript infers:
// params: { postId: string }
// search: validated search schema type
// loaderData: return type of loader
// context: router context type
})Authenticated Routes
鉴权路由
tsx
// __root.tsx
export const Route = createRootRouteWithContext<{
auth: AuthContext
}>()({
component: RootComponent,
})
// _authenticated.tsx (pathless layout for auth)
export const Route = createFileRoute('/_authenticated')({
beforeLoad: ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
},
})
// _authenticated/dashboard.tsx
export const Route = createFileRoute('/_authenticated/dashboard')({
component: Dashboard, // Only accessible when authenticated
})tsx
// __root.tsx
export const Route = createRootRouteWithContext<{
auth: AuthContext
}>()({
component: RootComponent,
})
// _authenticated.tsx (pathless layout for auth)
export const Route = createFileRoute('/_authenticated')({
beforeLoad: ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
},
})
// _authenticated/dashboard.tsx
export const Route = createFileRoute('/_authenticated/dashboard')({
component: Dashboard, // Only accessible when authenticated
})Scroll Restoration
滚动恢复
tsx
const router = createRouter({
routeTree,
// Enable scroll restoration
defaultScrollRestoration: true,
})
// Or per-route
export const Route = createFileRoute('/posts')({
// Scroll to top on navigation
scrollRestoration: true,
})
// Custom scroll restoration key
<ScrollRestoration
getKey={(location) => location.pathname}
/>tsx
const router = createRouter({
routeTree,
// Enable scroll restoration
defaultScrollRestoration: true,
})
// Or per-route
export const Route = createFileRoute('/posts')({
// Scroll to top on navigation
scrollRestoration: true,
})
// Custom scroll restoration key
<ScrollRestoration
getKey={(location) => location.pathname}
/>Route Masking
路由掩码
Display a different URL than the actual route:
tsx
<Link
to="/photos/$photoId"
params={{ photoId: photo.id }}
mask={{ to: '/photos', search: { photoId: photo.id } }}
>
View Photo
</Link>
// Or programmatically
navigate({
to: '/photos/$photoId',
params: { photoId: photo.id },
mask: { to: '/photos', search: { photoId: photo.id } },
})显示与实际路由不同的URL:
tsx
<Link
to="/photos/$photoId"
params={{ photoId: photo.id }}
mask={{ to: '/photos', search: { photoId: photo.id } }}
>
View Photo
</Link>
// Or programmatically
navigate({
to: '/photos/$photoId',
params: { photoId: photo.id },
mask: { to: '/photos', search: { photoId: photo.id } },
})Not Found Handling
404处理
tsx
// Global 404
const router = createRouter({
routeTree,
defaultNotFoundComponent: () => <div>Page not found</div>,
})
// Route-level 404
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
if (!post) throw notFound()
return post
},
notFoundComponent: () => <div>Post not found</div>,
})tsx
// Global 404
const router = createRouter({
routeTree,
defaultNotFoundComponent: () => <div>Page not found</div>,
})
// Route-level 404
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
if (!post) throw notFound()
return post
},
notFoundComponent: () => <div>Post not found</div>,
})Head Management
Head管理
tsx
export const Route = createFileRoute('/posts/$postId')({
head: ({ loaderData }) => ({
meta: [
{ title: loaderData.title },
{ name: 'description', content: loaderData.excerpt },
{ property: 'og:title', content: loaderData.title },
],
links: [
{ rel: 'canonical', href: `https://example.com/posts/${loaderData.id}` },
],
}),
})tsx
export const Route = createFileRoute('/posts/$postId')({
head: ({ loaderData }) => ({
meta: [
{ title: loaderData.title },
{ name: 'description', content: loaderData.excerpt },
{ property: 'og:title', content: loaderData.title },
],
links: [
{ rel: 'canonical', href: `https://example.com/posts/${loaderData.id}` },
],
}),
})Integration with TanStack Query
与TanStack Query集成
tsx
import { queryOptions } from '@tanstack/react-query'
const postsQueryOptions = queryOptions({
queryKey: ['posts'],
queryFn: fetchPosts,
})
export const Route = createFileRoute('/posts')({
loader: ({ context: { queryClient } }) => {
// Ensure data is in cache, won't refetch if fresh
return queryClient.ensureQueryData(postsQueryOptions)
},
component: PostsComponent,
})
function PostsComponent() {
// Use the same query options for reactive updates
const { data: posts } = useSuspenseQuery(postsQueryOptions)
return <PostsList posts={posts} />
}tsx
import { queryOptions } from '@tanstack/react-query'
const postsQueryOptions = queryOptions({
queryKey: ['posts'],
queryFn: fetchPosts,
})
export const Route = createFileRoute('/posts')({
loader: ({ context: { queryClient } }) => {
// Ensure data is in cache, won't refetch if fresh
return queryClient.ensureQueryData(postsQueryOptions)
},
component: PostsComponent,
})
function PostsComponent() {
// Use the same query options for reactive updates
const { data: posts } = useSuspenseQuery(postsQueryOptions)
return <PostsList posts={posts} />
}Router Hooks Reference
路由Hooks参考
| Hook | Purpose |
|---|---|
| Access router instance |
| Subscribe to router state |
| Get route path params |
| Get validated search params |
| Get route loader data |
| Get route context |
| Get navigate function |
| Get current location |
| Get all matched routes |
| Get specific route match |
| Block navigation |
| Get link props for custom components |
| Check if a route matches |
| Hook | 用途 |
|---|---|
| 获取路由实例 |
| 订阅路由状态 |
| 获取路由路径参数 |
| 获取已验证的搜索参数 |
| 获取路由Loader返回的数据 |
| 获取路由上下文 |
| 获取导航函数 |
| 获取当前位置信息 |
| 获取所有匹配的路由 |
| 获取特定路由的匹配信息 |
| 拦截导航操作 |
| 获取自定义组件的Link属性 |
| 检查路由是否匹配 |
Best Practices
最佳实践
- Use file-based routing for most applications - it's simpler and auto-generates the route tree
- Validate search params with Zod or custom validators for type safety
- Use to control when loaders re-execute based on search param changes
loaderDeps - Leverage context for dependency injection (QueryClient, auth state)
- Use for authentication guards, not in components
beforeLoad - Separate critical vs lazy code - keep loaders in the main file, components in
.lazy.tsx - Use on Links for perceived performance
preload="intent" - Use to prevent unnecessary refetches during navigation
staleTime - Register the router type for full TypeScript inference across the app
- Use instead of conditional rendering for 404 states
notFound() - Colocate search param logic with routes that own them
- Use pathless layouts () for shared auth/layout logic without URL segments
_authenticated
- 使用基于文件的路由:对于大多数应用来说更简单,会自动生成路由树
- 验证搜索参数:使用Zod或自定义验证器确保类型安全
- 使用:根据搜索参数的变化控制Loader的重新执行
loaderDeps - 利用上下文:实现依赖注入(如QueryClient、鉴权状态)
- 使用:在该钩子中处理鉴权守卫,不要在组件中处理
beforeLoad - 分离关键代码与延迟代码:将Loader放在主文件中,组件放在中
.lazy.tsx - 在Link上使用:提升感知性能
preload="intent" - 使用:避免导航时不必要的重新请求
staleTime - 注册路由类型:在整个应用中实现完整的TypeScript类型推导
- 使用:处理404状态时使用该方法,而非条件渲染
notFound() - 将搜索参数逻辑与路由放在一起:由对应的路由管理自身的搜索参数
- 使用无路径布局:如,用于共享鉴权/布局逻辑且不生成URL片段
_authenticated
Common Pitfalls
常见陷阱
- Forgetting to register the router type ()
declare module - Not using when loader depends on search params (causes stale data)
loaderDeps - Putting auth checks in components instead of (flash of protected content)
beforeLoad - Not handling the loading state with
pendingComponent - Using for data fetching instead of route loaders
useEffect - Mutating search params directly instead of using navigate/Link
- Not wrapping the app with
RouterProvider - Forgetting in code-based route definitions
getParentRoute
- 忘记注册路由类型(部分)
declare module - 当Loader依赖搜索参数时未使用(导致数据过期)
loaderDeps - 在组件中而非中处理鉴权检查(会出现受保护内容的闪现)
beforeLoad - 未使用处理加载状态
pendingComponent - 使用进行数据请求而非路由Loader
useEffect - 直接修改搜索参数而非使用navigate/Link
- 未用包裹应用
RouterProvider - 在基于代码的路由定义中忘记
getParentRoute