关于 path-params
面向 TanStack Router 开发者的技能,介绍动态、可选、通配及模式化 URL 参数,适用于定义类型化路由并安全地读取和传递参数。
主要功能
- 01动态及多个路径参数
- 02通配与可选参数
- 03前缀和后缀参数模式
- 04类型化参数访问与推断
- 05安全导航与编码
适用场景
- 01根据资源 ID 构建详情路由
- 02创建通配文件路由
- 03支持可选语言或分类路径段
- 04导航时保留类型化参数
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tanstack/ router --skill path-params安装后在 Claude Code 中输入/path-params确认已加载。
详细介绍
路径参数
路径参数会将动态 URL 片段捕获到命名变量中。在路由路径中,它们以 $ 为前缀定义。
CRITICAL:绝不要将参数插入
to字符串中。始终使用params属性。这是 Agent 处理路径参数时最常见的错误。
CRITICAL:类型会完全自动推断。绝不要为
useParams()的返回值添加类型注解。
动态片段
以 $ 为前缀的片段会捕获文本,直到遇到下一个 /。
// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
// params.postId is string — fully inferred, do not annotate
return fetchPost(params.postId)
},
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
const data = Route.useLoaderData()
return (
<h1>
Post {postId}: {data.title}
</h1>
)
}
多个动态片段可以跨路径层级使用:
// src/routes/teams.$teamId.members.$memberId.tsx
export const Route = createFileRoute('/teams/$teamId/members/$memberId')({
component: MemberComponent,
})
function MemberComponent() {
const { teamId, memberId } = Route.useParams()
return (
<div>
Team {teamId}, Member {memberId}
</div>
)
}
Splat / 捕获全部路由
路径以 $(单独的美元符号)结尾的路由会捕获其后的所有内容。该值可通过 _splat 键获取。
// src/routes/files.$.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/files/$')({
component: FileViewer,
})
function FileViewer() {
const { _splat } = Route.useParams()
// URL: /files/documents/report.pdf → _splat = "documents/report.pdf"
return <div>File path: {_splat}</div>
}
可选参数
可选参数使用 {-$paramName} 语法。该片段可以出现,也可以不出现。如果不存在,该值为 undefined。
// src/routes/posts.{-$category}.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/{-$category}')({
component: PostsComponent,
})
function PostsComponent() {
const { category } = Route.useParams()
// URL: /posts → category is undefined
// URL: /posts/tech → category is "tech"
return <div>{category ? `Posts in ${category}` : 'All Posts'}</div>
}
多个可选参数:
// Matches: /posts, /posts/tech, /posts/tech/hello-world
export const Route = createFileRoute('/posts/{-$category}/{-$slug}')({
component: PostComponent,
})
使用可选语言环境的 i18n
// src/routes/{-$locale}/about.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/{-$locale}/about')({
component: AboutComponent,
})
function AboutComponent() {
const { locale } = Route.useParams()
const currentLocale = locale || 'en'
return <h1>{currentLocale === 'fr' ? 'À Propos' : 'About Us'}</h1>
}
// Matches: /about, /en/about, /fr/about
前缀和后缀模式
在 $paramName 两侧使用花括号 {},即可在单个片段中让动态部分前后带有文本。
前缀
// src/routes/posts/post-{$postId}.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/post-{$postId}')({
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
// URL: /posts/post-123 → postId = "123"
return <div>Post ID: {postId}</div>
}
后缀
// src/routes/files/{$fileName}[.]txt.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/files/{$fileName}.txt')({
component: FileComponent,
})
function FileComponent() {
const { fileName } = Route.useParams()
// URL: /files/readme.txt → fileName = "readme"
return <div>File: {fileName}.txt</div>
}
前缀 + 后缀组合
// URL: /users/user-456.json → userId = "456"
export const Route = createFileRoute('/users/user-{$userId}.json')({
component: UserComponent,
})
function UserComponent() {
const { userId } = Route.useParams()
return <div>User: {userId}</div>
}
使用路径参数导航
对象形式
import { Link } from '@tanstack/react-router'
function PostLink({ postId }: { postId: string }) {
return (
<Link to="/posts/$postId" params={{ postId }}>
View Post
</Link>
)
}
函数形式(保留其他参数)
function PostLink({ postId }: { postId: string }) {
return (
<Link to="/posts/$postId" params={(prev) => ({ ...prev, postId })}>
View Post
</Link>
)
}
编程式导航
import { useNavigate } from '@tanstack/react-router'
function GoToPost({ postId }: { postId: string }) {
const navigate = useNavigate()
return (
<button
onClick={() => {
navigate({ to: '/posts/$postId', params: { postId } })
}}
>
Go to Post
</button>
)
}
使用可选参数导航
// Include the optional param
<Link to="/posts/{-$category}" params={{ category: 'tech' }}>
Tech Posts
</Link>
// Omit the optional param (renders /posts)
<Link to="/posts/{-$category}" params={{ category: undefined }}>
All Posts
</Link>
在路由组件外读取参数
使用带 from 的 useParams
import { useParams } from '@tanstack/react-router'
function PostHeader() {
const { postId } = useParams({ from: '/posts/$postId' })
return <h2>Post {postId}</h2>
}
使用带 strict: false 的 useParams
function GenericBreadcrumb() {
const params = useParams({ strict: false })
// params is a union of all possible route params
return <span>{params.postId ?? 'Home'}</span>
}
Loader 和 beforeLoad 中的参数
export const Route = createFileRoute('/posts/$postId')({
beforeLoad: async ({ params }) => {
// params.postId available here
const canView = await checkPermission(params.postId)
if (!canView) throw redirect({ to: '/unauthorized' })
},
loader: async ({ params }) => {
return fetchPost(params.postId)
},
})
允许的字符
默认情况下,参数使用 encodeURIComponent 编码。通过路由器配置允许额外字符:
import { createRouter } from '@tanstack/react-router'
const router = createRouter({
routeTree,
pathParamsAllowedCharacters: ['@', '+'],
})
允许的字符:;、:、@、&、=、+、$、,。
常见错误
1. CRITICAL(跨技能):将路径参数插入 to 字符串
// WRONG — breaks type safety and param encoding
<Link to={`/posts/${postId}`}>Post</Link>
// CORRECT — use params prop
<Link to="/posts/$postId" params={{ postId }}>Post</Link>
2. MEDIUM:使用 * 而不是 $ 表示 splat 路由
TanStack Router 使用 $ 表示 splat 路由。捕获的值位于 _splat 下,而不是 *。
// WRONG (React Router / other frameworks)
// <Route path="/files/*" />
// CORRECT (TanStack Router)
// File: src/routes/files.$.tsx
export const Route = createFileRoute('/files/$')({
component: () => {
const { _splat } = Route.useParams()
return <div>{_splat}</div>
},
})
注意:为保持向后兼容,v1 中仍支持
*,但 v2 中将移除。始终使用_splat。
3. MEDIUM:对基本动态片段使用花括号
花括号仅用于前缀/后缀模式和可选参数。基本动态片段使用不带花括号的 $。
// WRONG — braces not needed for basic params
createFileRoute('/posts/{$postId}')
// CORRECT — bare $ for basic dynamic segments
createFileRoute('/posts/$postId')
// CORRECT — braces for prefix pattern
createFileRoute('/posts/post-{$postId}')
// CORRECT — braces for optional param
createFileRoute('/posts/{-$category}')
4. 参数始终是字符串
路径参数始终解析为字符串。如果需要数字,请在 loader 或组件中进行解析:
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const id = parseInt(params.postId, 10)
if (isNaN(id)) throw notFound()
return fetchPost(id)
},
})
你也可以在路由上使用 params.parse 和 params.stringify 进行双向转换:
export const Route = createFileRoute('/posts/$postId')({
params: {
parse: (raw) => ({ postId: parseInt(raw.postId, 10) }),
stringify: (parsed) => ({ postId: String(parsed.postId) }),
},
loader: async ({ params }) => {
// params.postId is now number
return fetchPost(params.postId)
},
})