TanStack Router 07 ⚛️ Path Parameters — Dynamic URL segments
The previous chapter covered imperative navigation with the useNavigate hook, which is fully typed against the route tree. This chapter covers the path parameters: the dynamic segments that allow a single route file to serve multiple URLs. The TanStack Router’s handling of the path parameters is stricter than the React Router’s. The parameter is declared in the file name with the $ prefix (users.$id.tsx), and the type is inferred from the route definition. The Route.useParams() hook returns the typed params object. The params.parse and params.stringify options transform the param between the URL string and the application type. The critical trap is assuming the parameter is a number. The param from the URL is always a string by default. The component that calls an arithmetic operation on the raw id is the component that breaks. The params.parse option is the mechanism for the typed transformation at the route level.
Key point: The dynamic segment uses the $ prefix in the file name (users.$id.tsx) and the $id in the createFileRoute('/users/$id') path. The Route.useParams() returns the params object with the string values. The params.parse option transforms the value to the desired type (number, date, enum). The params.stringify option transforms it back for the navigation. The optional segment uses the {-$id} syntax (v1.81+) or the trailing $ with the custom parse for the backward compatibility. The splat uses the $ without a name ($.tsx).
Why path parameters require a specific mental model
The $ prefix problem. The dynamic segment is the file name with the $ prefix. The users.$id.tsx file becomes the /users/:id route in the React Router syntax, or the /users/$id route in the TanStack Router syntax. The $id is the parameter name. The createFileRoute('/users/$id') path must match the file location exactly. The mismatch is a generation error.
The string problem. The Route.useParams() returns the { id: "123" } object, not { id: 123 }. The value is a string. The component that uses the id in the arithmetic, the array index, or the API path must parse it. The Number(params.id) or the params.parse option is the conversion.
The params.parse problem. The params.parse option is the route-level transformation. The function receives the raw string params and returns the typed params. The params.stringify is the reverse transformation, which is used when the route is navigated to with the typed param. The useNavigate({ to: '/users/$id', params: { id: 123 } }) passes the number if the params.parse is configured, and the router stringifies it for the URL.
The optional parameter problem. The optional segment was added in the TanStack Router v1.81 as the {-$id} syntax. The older versions used the $id? syntax with the custom parse function. The {-$id} is the current form. The params.id is undefined when the segment is absent. The component must handle the undefined case.
The splat problem. The $ without a name is the splat. The $.tsx file matches any URL under the root. The Route.useParams() returns the _splat property with the unmatched path. The splat is the 404 fallback and the file browser.
The trade-off. The typed params.parse is the safer approach because the route definition is the single source of truth for the transformation. The component-level Number(...) is the quick approach for the simple cases. The route-level params.parse is the recommended approach because the type safety extends to the Link and navigate calls.
a. The basic dynamic route and the Route.useParams
The dynamic route is the file with the $ prefix. The Route.useParams() returns the params.
// src/routes/users.$id.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/users/$id')({
component: UserProfile,
})
function UserProfile() {
const { id } = Route.useParams()
return <h1>User: {id}</h1>
}
The /users/123 URL matches the route. The Route.useParams() returns { id: '123' }. The component renders User: 123.
The createFileRoute('/users/$id') path is the exact file location. The users.$id.tsx file is the matching file. The plugin uses the file to generate the route tree.
b. The params.parse and the typed params
The params.parse option transforms the string param to the desired type.
// src/routes/users.$id.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/users/$id')({
params: {
parse: (params) => ({ id: Number(params.id) }),
stringify: (params) => ({ id: String(params.id) }),
},
component: UserProfile,
})
function UserProfile() {
const { id } = Route.useParams()
// id is typed as number
return <h1>User: {id}</h1>
}
The parse function receives the raw params ({ id: '123' }) and returns the typed params ({ id: 123 }). The stringify function is the reverse. The Route.useParams() returns the typed { id: 123 }.
The typed params propagate to the Link and navigate calls. The <Link to="/users/$id" params={{ id: 123 }}> passes the number. The navigate({ to: '/users/$id', params: { id: 123 } }) passes the number. The type safety is the benefit.
The invalid value causes the route to throw. The parse function can throw a notFound() or a redirect() if the value is not what the route expects.
parse: (params) => {
const id = Number(params.id)
if (Number.isNaN(id)) throw notFound()
return { id }
},
The notFound() is the TanStack Router’s function that triggers the notFoundComponent. The invalid ID renders the not-found page, not a broken component.
c. The optional segment and the splat
The optional segment uses the {-$id} syntax in the file name and the path.
// src/routes/posts.{-$month}.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/{-$month}')({
component: PostsArchive,
})
function PostsArchive() {
const { month } = Route.useParams()
return <p>Month: {month ?? 'all'}</p>
}
The {-$month} is the optional segment. The /posts/2026 URL matches the route with the month param. The /posts URL matches the route with the month param as undefined. The component uses the month ?? 'all' fallback.
The splat is the $ without a name. The $.tsx file matches any URL under the root.
// src/routes/$.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/$')({
component: NotFound,
})
function NotFound() {
const { _splat } = Route.useParams()
return <h1>404: {_splat}</h1>
}
The _splat is the property name. The Route.useParams() returns { _splat: 'unknown/path' }. The component renders the unmatched path.
Complete Example Session
# ============================================
# PART 1: BASIC DYNAMIC ROUTE
# ============================================
// src/routes/users.$id.tsx
export const Route = createFileRoute('/users/$id')({
component: UserProfile,
})
function UserProfile() {
const { id } = Route.useParams()
return <h1>User: {id}</h1>
}
# ============================================
# PART 2: TYPED PARAMS
# ============================================
export const Route = createFileRoute('/users/$id')({
params: { parse: (p) => ({ id: Number(p.id) }) },
component: UserProfile,
})
const { id } = Route.useParams()
// id: number
# ============================================
# PART 3: TYPED LINK
# ============================================
<Link to="/users/$id" params={{ id: 123 }}>User</Link>
# ============================================
# PART 4: INVALID PARAM
# ============================================
params: {
parse: (p) => {
const id = Number(p.id)
if (Number.isNaN(id)) throw notFound()
return { id }
},
}
# ============================================
# PART 5: NESTED PARAMS
# ============================================
// src/routes/users.$userId.posts.$postId.tsx
const { userId, postId } = Route.useParams()
# ============================================
# PART 6: OPTIONAL SEGMENT
# ============================================
// src/routes/posts.{-$month}.tsx
const { month } = Route.useParams()
return <p>{month ?? 'all'}</p>
# ============================================
# PART 7: SPLAT
# ============================================
// src/routes/$.tsx
const { _splat } = Route.useParams()
return <h1>404: {_splat}</h1>
# ============================================
# PART 8: useParams from useParams hook
# ============================================
import { useParams } from '@tanstack/react-router'
const { id } = useParams({ from: '/users/$id' })
# ============================================
# PART 9: VERIFY
# ============================================
# /users/123 → id: 123 (number)
# /users/abc → notFound
# /posts → month: undefined
# /posts/10 → month: 10
# /unknown → splat: unknown
The nine parts trace the path parameter: basic, typed, typed link, invalid, nested, optional, splat, hook, and verification.
Quick Reference
File Naming
| File | URL |
|---|---|
users.$id.tsx | /users/$id |
users.$userId.posts.$postId.tsx | /users/$userId/posts/$postId |
posts.{-$month}.tsx | /posts/{-$month} |
$.tsx | /$ |
Hooks
| Hook | Returns |
|---|---|
Route.useParams() | Typed params |
useParams({ from }) | Typed params (external) |
Params Options
| Option | Purpose |
|---|---|
parse | String → typed |
stringify | Typed → string |
Special Functions
| Function | Purpose |
|---|---|
notFound() | Trigger 404 |
redirect() | Redirect from parse |
_splat | Splat param name |
Best Practices
✅ Do This:
params: { parse: (p) => ({ id: Number(p.id) }) }
<!-- Parse the param at the route level --> # ✅
const { id } = Route.useParams()
<!-- Use the typed hook --> # ✅
if (Number.isNaN(id)) throw notFound()
<!-- Validate and throw notFound --> # ✅
❌ Don’t Do This:
const { id } = Route.useParams()
array[id]
<!-- String index on array --> # ❌
<Link to="/users/$id" params={{ id: "123" }}>
# When parse expects number
<!-- Type error --> # ❌
<Link to="/users/$id">
<!-- Missing params --> # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
Type error on params | Missing or wrong type | Use parse |
| Array index wrong | String used as number | parse or Number |
undefined crash | Optional segment absent | ?? default |
| Splat empty | Wrong path | Use $ |
| Typed link error | params type mismatch | Match the parse |
Real-World Examples
1. Basic Param
const { id } = Route.useParams()
2. Typed Param
params: { parse: (p) => ({ id: Number(p.id) }) }
3. Typed Link
<Link to="/users/$id" params={{ id: 123 }}>User</Link>
4. Nested Params
const { userId, postId } = Route.useParams()
5. Optional Segment
const { month } = Route.useParams()
6. Splat
const { _splat } = Route.useParams()
7. notFound
if (Number.isNaN(id)) throw notFound()
8. useParams Hook
const { id } = useParams({ from: '/users/$id' })
9. Stringify
params: { stringify: (p) => ({ id: String(p.id) }) }
10. Navigate with Params
navigate({ to: '/users/$id', params: { id: 123 } })
Visual
Parameter Flow
┌──────────────────────────────────────────────┐
│ URL: /users/123 │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Route: /users/$id │
│ → Raw params: { id: "123" } │
└──────────────────┬───────────────────────────┘
│
│ params.parse
▼
┌──────────────────────────────────────────────┐
│ Typed params: { id: 123 } │
└──────────────────┬───────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ Component: Route.useParams() │
│ → { id: 123 } (number) │
└──────────────────────────────────────────────┘
File to URL
┌──────────────────────────────────────────────┐
│ src/routes/ │
│ ├── users.$id.tsx → /users/$id │
│ ├── users.$id.posts.$postId.tsx → nested │
│ ├── posts.{-$month}.tsx → optional │
│ └── $.tsx → splat │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Dynamic file | users.$id.tsx |
| Dynamic path | createFileRoute('/users/$id') |
| Params hook | Route.useParams() |
| Parse option | params.parse |
| Stringify option | params.stringify |
| Optional segment | {-$month} |
| Splat | $ without name |
| Splat param | _splat |
Key takeaways:
- The dynamic segment uses the
$prefix. Theusers.$id.tsxfile becomes the/users/$idroute. The$idis the parameter name. ThecreateFileRoute('/users/$id')path must match the file. - The
Route.useParams()returns the string params. Theparams.parseoption transforms them to the desired type. The typed params propagate to theLinkandnavigatecalls. - The
params.parsecan thrownotFound(). The invalid value (e.g.,Number("abc")returnsNaN) triggers thenotFoundComponent. The component never receives the invalid value. - The optional segment uses the
{-$month}syntax. Themonthparam isundefinedwhen the segment is absent. The component handles theundefinedcase with the?? defaultfallback. - The splat uses the
$without a name. The$.tsxfile matches any URL under the root. The_splatproperty contains the unmatched path.
Remember: The $ is the dynamic. The parse is the type. The useParams is the read. The notFound is the invalid. The {-$id} is the optional. The _splat is the wildcard. The exam tests the $ prefix and the params.parse.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!