| | |

TanStack Router 07 ⚛️ Path Parameters — Dynamic URL segments

⏱️ Estimated read: 9 min | |

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

FileURL
users.$id.tsx/users/$id
users.$userId.posts.$postId.tsx/users/$userId/posts/$postId
posts.{-$month}.tsx/posts/{-$month}
$.tsx/$

Hooks

HookReturns
Route.useParams()Typed params
useParams({ from })Typed params (external)

Params Options

OptionPurpose
parseString → typed
stringifyTyped → string

Special Functions

FunctionPurpose
notFound()Trigger 404
redirect()Redirect from parse
_splatSplat 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

PitfallWhy It HappensFix
Type error on paramsMissing or wrong typeUse parse
Array index wrongString used as numberparse or Number
undefined crashOptional segment absent?? default
Splat emptyWrong pathUse $
Typed link errorparams type mismatchMatch 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

ItemValue
Dynamic fileusers.$id.tsx
Dynamic pathcreateFileRoute('/users/$id')
Params hookRoute.useParams()
Parse optionparams.parse
Stringify optionparams.stringify
Optional segment{-$month}
Splat$ without name
Splat param_splat

Key takeaways:

  • The dynamic segment uses the $ prefix. The users.$id.tsx file becomes the /users/$id route. The $id is the parameter name. The createFileRoute('/users/$id') path must match the file.
  • The Route.useParams() returns the string params. The params.parse option transforms them to the desired type. The typed params propagate to the Link and navigate calls.
  • The params.parse can throw notFound(). The invalid value (e.g., Number("abc") returns NaN) triggers the notFoundComponent. The component never receives the invalid value.
  • The optional segment uses the {-$month} syntax. The month param is undefined when the segment is absent. The component handles the undefined case with the ?? default fallback.
  • The splat uses the $ without a name. The $.tsx file matches any URL under the root. The _splat property 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.

Was this helpful?


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!