tanstack-router by tanstack-skills/tanstack-skills
npx skills add https://github.com/tanstack-skills/tanstack-skills --skill tanstack-routerTanStack Router 是一个完全类型安全的 React(和 Solid)应用程序路由器。它提供基于文件的路由、一流的搜索参数管理、内置数据加载、代码分割和深度 TypeScript 集成。它作为 TanStack Start(全栈框架)的路由基础。
包: @tanstack/react-router CLI: @tanstack/router-cli 或 @tanstack/router-plugin (Vite/Rspack/Webpack) 开发者工具: @tanstack/react-router-devtools
npm install @tanstack/react-router
# 用于 Vite 的基于文件的路由:
npm install -D @tanstack/router-plugin
# 或者独立的 CLI:
npm install -D @tanstack/router-cli
路由以树形结构组织。根路由是顶级布局,子路由嵌套在其下方。
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 })
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
基于文件的路由会根据你的文件结构自动生成路由树。通过 Vite 插件配置:
// vite.config.ts
import { defineConfig } from 'vite'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
TanStackRouterVite(),
// ... 其他插件
],
})
| 文件模式 | 路由类型 | 示例路径 |
|---|---|---|
__root.tsx | 根布局 | N/A(包裹所有内容) |
index.tsx | 索引路由 | / |
about.tsx | 静态路由 | /about |
$postId.tsx | 动态参数 | /posts/$postId |
posts.tsx | 布局路由 | /posts/*(布局) |
posts/index.tsx | 嵌套索引 | /posts |
posts/$postId.tsx | 嵌套动态 | /posts/123 |
posts_.$postId.tsx | 无路径布局 | /posts/123(不同的布局) |
_layout.tsx | 无路径布局 | N/A(分组路由) |
_layout/dashboard.tsx | 分组路由 | /dashboard |
$.tsx | 通配符/捕获所有 | /* |
posts.$postId.edit.tsx | 点表示法 | /posts/123/edit |
_ 前缀:无路径路由(没有 URL 片段的布局分组)$ 前缀:动态路径参数(folder) 括号:路由组(组织性,不影响 URL)每个路由可以定义:
// routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// 路径参数验证
params: {
parse: (params) => ({ postId: Number(params.postId) }),
stringify: (params) => ({ postId: String(params.postId) }),
},
// 搜索参数验证
validateSearch: (search: Record<string, unknown>) => {
return {
page: Number(search.page ?? 1),
filter: (search.filter as string) || '',
}
},
// 数据加载
loader: async ({ params, context, abortController }) => {
return fetchPost(params.postId)
},
// 加载器依赖项(当这些变化时重新运行加载器)
loaderDeps: ({ search }) => ({ page: search.page }),
// 缓存的加载器数据的过期时间
staleTime: 5_000,
// 预加载
preloadStaleTime: 30_000,
// 错误组件
errorComponent: PostErrorComponent,
// 挂起/加载组件
pendingComponent: PostLoadingComponent,
// 404 组件
notFoundComponent: PostNotFoundComponent,
// 加载前钩子(身份验证、重定向)
beforeLoad: async ({ context, location }) => {
if (!context.auth.isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: location.href },
})
}
},
// Head/元数据管理
head: () => ({
meta: [{ title: 'Post Details' }],
}),
// 组件
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
const post = Route.useLoaderData()
const { page, filter } = Route.useSearch()
return <div>{post.title}</div>
}
export const Route = createFileRoute('/posts')({
loader: async ({ context }) => {
// 访问路由器上下文(例如,queryClient)
const posts = await context.queryClient.ensureQueryData({
queryKey: ['posts'],
queryFn: fetchPosts,
})
return { posts }
},
component: PostsComponent,
})
function PostsComponent() {
const { posts } = Route.useLoaderData()
// ...
}
控制加载器何时重新执行:
export const Route = createFileRoute('/posts')({
loaderDeps: ({ search: { page, filter } }) => ({ page, filter }),
loader: async ({ deps: { page, filter } }) => {
return fetchPosts({ page, filter })
},
})
流式传输非关键数据:
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>
)
}
通过路由器上下文提供共享依赖项:
// 创建带上下文的 router
const router = createRouter({
routeTree,
context: {
queryClient,
auth: undefined!, // 将由 RouterProvider 提供
},
})
// 在根/应用组件中
function App() {
const auth = useAuth()
return <RouterProvider router={router} context={{ auth }} />
}
// 在路由中
export const Route = createFileRoute('/protected')({
beforeLoad: ({ context }) => {
if (!context.auth.user) throw redirect({ to: '/login' })
},
loader: ({ context }) => {
return context.queryClient.ensureQueryData(userQueryOptions())
},
})
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,
// 或手动验证:
// validateSearch: (search) => postSearchSchema.parse(search),
})
function PostsComponent() {
// 从路由中读取
const { page, filter, sort } = Route.useSearch()
// 或者使用 useSearch hook 从任何组件读取
const search = useSearch({ from: '/posts' })
}
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 }),
})
}
>
下一页
</button>
)
}
// 或者通过 Link 组件
<Link
to="/posts"
search={(prev) => ({ ...prev, page: 2 })}
>
第 2 页
</Link>
const router = createRouter({
routeTree,
// 自定义序列化
search: {
strict: true, // 拒绝未知参数
},
// 默认搜索参数序列化器
stringifySearch: defaultStringifySearch,
parseSearch: defaultParseSearch,
})
import { Link } from '@tanstack/react-router'
// 静态路由
<Link to="/about">关于</Link>
// 带参数的动态路由
<Link to="/posts/$postId" params={{ postId: '123' }}>
文章 123
</Link>
// 带搜索参数
<Link to="/posts" search={{ page: 2, filter: 'react' }}>
第 2 页
</Link>
// 活动链接样式
<Link
to="/posts"
activeProps={{ className: 'active' }}
inactiveProps={{ className: 'inactive' }}
activeOptions={{ exact: true }}
>
文章
</Link>
// 预加载
<Link to="/posts" preload="intent">文章</Link>
<Link to="/dashboard" preload="viewport">仪表板</Link>
// 哈希
<Link to="/docs" hash="api-reference">API 参考</Link>
import { useNavigate, useRouter } from '@tanstack/react-router'
function MyComponent() {
const navigate = useNavigate()
const router = useRouter()
// 导航到路由
navigate({ to: '/posts', search: { page: 1 } })
// 替换导航
navigate({ to: '/posts', replace: true })
// 相对导航
navigate({ to: '.', search: (prev) => ({ ...prev, page: 2 }) })
// 后退/前进
router.history.back()
router.history.forward()
// 使当前路由失效并重新加载
router.invalidate()
}
import { redirect } from '@tanstack/react-router'
// 在 beforeLoad 或 loader 中
throw redirect({
to: '/login',
search: { redirect: location.href },
// 可选状态码
statusCode: 301, // 永久重定向 (SSR)
})
import { useBlocker } from '@tanstack/react-router'
function FormComponent() {
const [isDirty, setIsDirty] = useState(false)
useBlocker({
shouldBlockFn: () => isDirty,
withResolver: true, // 显示确认对话框
})
// 或者使用自定义 UI
const { proceed, reset, status } = useBlocker({
shouldBlockFn: () => isDirty,
})
if (status === 'blocked') {
return (
<div>
<p>确定要离开吗?</p>
<button onClick={proceed}>离开</button>
<button onClick={reset}>留下</button>
</div>
)
}
}
使用基于文件的路由,创建一个惰性文件:
routes/
posts.tsx # 关键:loader, beforeLoad, meta
posts.lazy.tsx # 惰性:component, pendingComponent, errorComponent
// posts.tsx(急切加载)
export const Route = createFileRoute('/posts')({
loader: () => fetchPosts(),
})
// posts.lazy.tsx(惰性加载)
import { createLazyFileRoute } from '@tanstack/react-router'
export const Route = createLazyFileRoute('/posts')({
component: PostsComponent,
pendingComponent: PostsLoading,
errorComponent: PostsError,
})
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
loader: () => fetchPosts(),
}).lazy(() => import('./posts.lazy').then((d) => d.Route))
// 路由器级别默认值
const router = createRouter({
routeTree,
defaultPreload: 'intent', // 'intent' | 'viewport' | 'render' | false
defaultPreloadStaleTime: 30_000, // 30 秒
})
// 路由级别
export const Route = createFileRoute('/posts/$postId')({
// 加载器数据的过期时间
staleTime: 5_000,
// 预加载数据保持新鲜的时间
preloadStaleTime: 30_000,
})
// Link 级别
<Link to="/posts" preload="intent" preloadDelay={100}>
文章
</Link>
// 为类型推断声明模块
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
所有 hooks 都基于路由树完全类型化:
// useParams - 类型化为路由的参数
const { postId } = useParams({ from: '/posts/$postId' })
// useSearch - 类型化为路由的搜索模式
const { page } = useSearch({ from: '/posts' })
// useLoaderData - 类型化为加载器的返回
const data = useLoaderData({ from: '/posts/$postId' })
// useRouteContext - 类型化为路由上下文
const { auth } = useRouteContext({ from: '/protected' })
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
// TypeScript 推断:
// params: { postId: string }
// search: 已验证的搜索模式类型
// loaderData: loader 的返回类型
// context: 路由器上下文类型
})
// __root.tsx
export const Route = createRootRouteWithContext<{
auth: AuthContext
}>()({
component: RootComponent,
})
// _authenticated.tsx(用于身份验证的无路径布局)
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, // 仅在身份验证后可访问
})
const router = createRouter({
routeTree,
// 启用滚动恢复
defaultScrollRestoration: true,
})
// 或者按路由配置
export const Route = createFileRoute('/posts')({
// 导航时滚动到顶部
scrollRestoration: true,
})
// 自定义滚动恢复键
<ScrollRestoration
getKey={(location) => location.pathname}
/>
显示与实际路由不同的 URL:
<Link
to="/photos/$photoId"
params={{ photoId: photo.id }}
mask={{ to: '/photos', search: { photoId: photo.id } }}
>
查看照片
</Link>
// 或者以编程方式
navigate({
to: '/photos/$photoId',
params: { photoId: photo.id },
mask: { to: '/photos', search: { photoId: photo.id } },
})
// 全局 404
const router = createRouter({
routeTree,
defaultNotFoundComponent: () => <div>页面未找到</div>,
})
// 路由级别 404
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
if (!post) throw notFound()
return post
},
notFoundComponent: () => <div>文章未找到</div>,
})
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}` },
],
}),
})
import { queryOptions } from '@tanstack/react-query'
const postsQueryOptions = queryOptions({
queryKey: ['posts'],
queryFn: fetchPosts,
})
export const Route = createFileRoute('/posts')({
loader: ({ context: { queryClient } }) => {
// 确保数据在缓存中,如果数据新鲜则不会重新获取
return queryClient.ensureQueryData(postsQueryOptions)
},
component: PostsComponent,
})
function PostsComponent() {
// 使用相同的查询选项进行响应式更新
const { data: posts } = useSuspenseQuery(postsQueryOptions)
return <PostsList posts={posts} />
}
| Hook | 用途 |
|---|---|
useRouter() | 访问路由器实例 |
useRouterState() | 订阅路由器状态 |
useParams() | 获取路由路径参数 |
useSearch() | 获取已验证的搜索参数 |
useLoaderData() | 获取路由加载器数据 |
useRouteContext() | 获取路由上下文 |
useNavigate() | 获取导航函数 |
useLocation() | 获取当前位置 |
useMatches() | 获取所有匹配的路由 |
useMatch() | 获取特定的路由匹配 |
useBlocker() | 拦截导航 |
useLinkProps() | 获取自定义组件的链接属性 |
useMatchRoute() | 检查路由是否匹配 |
loaderDeps 根据搜索参数变化控制加载器重新执行beforeLoad 进行身份验证守卫,而不是在组件中.lazy.tsx 中preload="intent" 以提高感知性能staleTime 防止导航期间不必要的重新获取notFound() 而不是条件渲染来处理 404 状态_authenticated) 实现共享的身份验证/布局逻辑,而无需 URL 片段declare module)loaderDeps(导致数据过时)beforeLoad 中(导致受保护内容闪现)pendingComponent 处理加载状态useEffect 进行数据获取而不是路由加载器RouterProvider 包裹应用程序getParentRoute每周安装量
245
仓库
GitHub 星标数
5
首次出现
2026年2月21日
安全审计
安装于
codex234
opencode233
cursor233
gemini-cli232
github-copilot231
kimi-cli231
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: @tanstack/react-router CLI: @tanstack/router-cli or @tanstack/router-plugin (Vite/Rspack/Webpack) Devtools: @tanstack/react-router-devtools
npm install @tanstack/react-router
# For file-based routing with Vite:
npm install -D @tanstack/router-plugin
# Or standalone CLI:
npm install -D @tanstack/router-cli
Routes are organized in a tree structure. The root route is the top-level layout, and child routes nest underneath.
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 automatically generates the route tree from your file structure. Configure with Vite plugin:
// vite.config.ts
import { defineConfig } from 'vite'
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
export default defineConfig({
plugins: [
TanStackRouterVite(),
// ... other plugins
],
})
| File Pattern | Route Type | Example Path |
|---|---|---|
__root.tsx | Root layout | N/A (wraps all) |
index.tsx | Index route | / |
about.tsx | Static route | /about |
$postId.tsx | Dynamic param | /posts/$postId |
_ prefix: Pathless routes (layout groups without URL segment)$ prefix: Dynamic path parameters(folder) parentheses: Route groups (organizational, no URL impact)Each route can define:
// 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>
}
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()
// ...
}
Control when loaders re-execute:
export const Route = createFileRoute('/posts')({
loaderDeps: ({ search: { page, filter } }) => ({ page, filter }),
loader: async ({ deps: { page, filter } }) => {
return fetchPosts({ page, filter })
},
})
Stream non-critical data:
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>
)
}
Provide shared dependencies via router context:
// 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())
},
})
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),
})
function PostsComponent() {
// From route
const { page, filter, sort } = Route.useSearch()
// Or from any component with useSearch hook
const search = useSearch({ from: '/posts' })
}
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>
const router = createRouter({
routeTree,
// Custom serialization
search: {
strict: true, // Reject unknown params
},
// Default search param serializer
stringifySearch: defaultStringifySearch,
parseSearch: defaultParseSearch,
})
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>
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()
}
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)
})
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>
)
}
}
With file-based routing, create a lazy file:
routes/
posts.tsx # Critical: loader, beforeLoad, meta
posts.lazy.tsx # Lazy: component, pendingComponent, errorComponent
// 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,
})
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
loader: () => fetchPosts(),
}).lazy(() => import('./posts.lazy').then((d) => d.Route))
// 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>
// Declare module for type inference
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
All hooks are fully typed based on the route tree:
// 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' })
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
})
// __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
})
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}
/>
Display a different URL than the actual route:
<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 } },
})
// 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>,
})
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}` },
],
}),
})
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} />
}
| Hook | Purpose |
|---|---|
useRouter() | Access router instance |
useRouterState() | Subscribe to router state |
useParams() | Get route path params |
useSearch() | Get validated search params |
useLoaderData() | Get route loader data |
useRouteContext() | Get route context |
loaderDeps to control when loaders re-execute based on search param changesbeforeLoad for authentication guards, not in components.lazy.tsxpreload="intent" on Links for perceived performancestaleTime to prevent unnecessary refetches during navigationnotFound() instead of conditional rendering for 404 statesdeclare module)loaderDeps when loader depends on search params (causes stale data)beforeLoad (flash of protected content)pendingComponentuseEffect for data fetching instead of route loadersRouterProvidergetParentRoute in code-based route definitionsWeekly Installs
245
Repository
GitHub Stars
5
First Seen
Feb 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex234
opencode233
cursor233
gemini-cli232
github-copilot231
kimi-cli231
Flutter应用架构设计指南:分层结构、数据层实现与最佳实践
3,400 周安装
posts.tsx | Layout route | /posts/* (layout) |
posts/index.tsx | Nested index | /posts |
posts/$postId.tsx | Nested dynamic | /posts/123 |
posts_.$postId.tsx | Pathless layout | /posts/123 (different layout) |
_layout.tsx | Pathless layout | N/A (groups routes) |
_layout/dashboard.tsx | Grouped route | /dashboard |
$.tsx | Splat/catch-all | /* |
posts.$postId.edit.tsx | Dot notation | /posts/123/edit |
useNavigate() | Get navigate function |
useLocation() | Get current location |
useMatches() | Get all matched routes |
useMatch() | Get specific route match |
useBlocker() | Block navigation |
useLinkProps() | Get link props for custom components |
useMatchRoute() | Check if a route matches |
_authenticated) for shared auth/layout logic without URL segments