| name | router-core/path-params |
| description | Dynamic path segments ($paramName), splat routes ($ / _splat), optional params ({-$paramName}), prefix/suffix patterns ({$param}.ext), useParams, params.parse/stringify, pathParamsAllowedCharacters, i18n locale patterns. |
| type | sub-skill |
| library | tanstack-router |
| library_version | 1.166.2 |
| requires | ["router-core"] |
| sources | ["TanStack/router:docs/router/guide/path-params.md","TanStack/router:docs/router/routing/routing-concepts.md","TanStack/router:docs/router/guide/internationalization-i18n.md"] |
Path Params
Path params capture dynamic URL segments into named variables. They are defined with a $ prefix in the route path.
CRITICAL: Never interpolate params into the to string. Always use the params prop. This is the most common agent mistake for path params.
CRITICAL: Types are fully inferred. Never annotate the return of useParams().
Dynamic Segments
A segment prefixed with $ captures text until the next /.
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
return fetchPost(params.postId)
},
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
const data = Route.useLoaderData()
return (
<h1>
Post {postId}: {data.title}
</h1>
)
}
Multiple dynamic segments work across path levels:
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 / Catch-All Routes
A route with a path ending in $ (bare dollar sign) captures everything after it. The value is available under the _splat key.
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/files/$')({
component: FileViewer,
})
function FileViewer() {
const { _splat } = Route.useParams()
return <div>File path: {_splat}</div>
}
Optional Params
Optional params use {-$paramName} syntax. The segment may or may not be present. When absent, the value is undefined.
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/{-$category}')({
component: PostsComponent,
})
function PostsComponent() {
const { category } = Route.useParams()
return <div>{category ? `Posts in ${category}` : 'All Posts'}</div>
}
Multiple optional params:
export const Route = createFileRoute('/posts/{-$category}/{-$slug}')({
component: PostComponent,
})
i18n with Optional Locale
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>
}
Prefix and Suffix Patterns
Curly braces {} around $paramName allow text before or after the dynamic part within a single segment.
Prefix
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/post-{$postId}')({
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
return <div>Post ID: {postId}</div>
}
Suffix
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/files/{$fileName}.txt')({
component: FileComponent,
})
function FileComponent() {
const { fileName } = Route.useParams()
return <div>File: {fileName}.txt</div>
}
Combined Prefix + Suffix
export const Route = createFileRoute('/users/user-{$userId}.json')({
component: UserComponent,
})
function UserComponent() {
const { userId } = Route.useParams()
return <div>User: {userId}</div>
}
Navigating with Path Params
Object Form
import { Link } from '@tanstack/react-router'
function PostLink({ postId }: { postId: string }) {
return (
<Link to="/posts/$postId" params={{ postId }}>
View Post
</Link>
)
}
Function Form (Preserves Other Params)
function PostLink({ postId }: { postId: string }) {
return (
<Link to="/posts/$postId" params={(prev) => ({ ...prev, postId })}>
View Post
</Link>
)
}
Programmatic Navigation
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>
)
}
Navigating with Optional Params
<Link to="/posts/{-$category}" params={{ category: 'tech' }}>
Tech Posts
</Link>
<Link to="/posts/{-$category}" params={{ category: undefined }}>
All Posts
</Link>
Reading Params Outside Route Components
useParams with from
import { useParams } from '@tanstack/react-router'
function PostHeader() {
const { postId } = useParams({ from: '/posts/$postId' })
return <h2>Post {postId}</h2>
}
useParams with strict: false
function GenericBreadcrumb() {
const params = useParams({ strict: false })
return <span>{params.postId ?? 'Home'}</span>
}
Params in Loaders and beforeLoad
export const Route = createFileRoute('/posts/$postId')({
beforeLoad: async ({ params }) => {
const canView = await checkPermission(params.postId)
if (!canView) throw redirect({ to: '/unauthorized' })
},
loader: async ({ params }) => {
return fetchPost(params.postId)
},
})
Allowed Characters
By default, params are encoded with encodeURIComponent. Allow extra characters via router config:
import { createRouter } from '@tanstack/react-router'
const router = createRouter({
routeTree,
pathParamsAllowedCharacters: ['@', '+'],
})
Allowed characters: ;, :, @, &, =, +, $, ,.
Common Mistakes
1. CRITICAL (cross-skill): Interpolating path params into to string
<Link to={`/posts/${postId}`}>Post</Link>
<Link to="/posts/$postId" params={{ postId }}>Post</Link>
2. MEDIUM: Using * for splat routes instead of $
TanStack Router uses $ for splat routes. The captured value is under _splat, not *.
export const Route = createFileRoute('/files/$')({
component: () => {
const { _splat } = Route.useParams()
return <div>{_splat}</div>
},
})
Note: * works in v1 for backwards compatibility but will be removed in v2. Always use _splat.
3. MEDIUM: Using curly braces for basic dynamic segments
Curly braces are ONLY for prefix/suffix patterns and optional params. Basic dynamic segments use bare $.
createFileRoute('/posts/{$postId}')
createFileRoute('/posts/$postId')
createFileRoute('/posts/post-{$postId}')
createFileRoute('/posts/{-$category}')
4. Params are always strings
Path params are always parsed as strings. If you need a number, parse in the loader or component:
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => {
const id = parseInt(params.postId, 10)
if (isNaN(id)) throw notFound()
return fetchPost(id)
},
})
You can also use params.parse and params.stringify on the route for bidirectional transformation:
export const Route = createFileRoute('/posts/$postId')({
params: {
parse: (raw) => ({ postId: parseInt(raw.postId, 10) }),
stringify: (parsed) => ({ postId: String(parsed.postId) }),
},
loader: async ({ params }) => {
return fetchPost(params.postId)
},
})