| | |

React Router 07 ⚛️ Path Parameters — Dynamic URL segments

⏱️ Estimated read: 10 min | |

The previous chapter covered active styling. This chapter covers the mechanism that makes a single route component serve multiple URLs: the path parameter. A path parameter is a dynamic segment in the route’s path, marked with a colon prefix (:id). The router matches any single URL segment to the parameter, and the component reads the value with the useParams hook. The LFCS exam does not test React Router, but front-end engineering certification paths do. The critical detail is the distinction between the parameter value (always a string) and the parsed value (a number, a date, or a custom type). The router does not parse the parameter. The useParams hook returns strings. The component must validate and parse the value before using it. A path parameter of /users/:id returns "1" for /users/1, not 1. The component that treats the string as a number without parsing is the component that breaks on /users/abc.

Key point: The :id in the route path is the parameter name. The useParams hook returns an object with the parameter name as the key and the matched URL segment as the value. The value is always a string. The component must parse and validate the value before using it. The optional parameter uses the ? suffix (:id?). The splat uses the * wildcard. The parent route’s parameters are available in the child route’s component.


Why path parameters require a specific mental model

The string problem. The parameter is a string. The /users/:id route returns { id: "1" } for /users/1, not { id: 1 }. The component that calls fetch(\/api/users/${id}`)works because the string is interpolated. The component that callsarray[id]with a string fails silently because the array index expects a number, and the string“1”is not the same as the number1. The Number(id)orparseInt(id, 10)conversion is the fix. The validation is the second fix: the/users/abcURL matches the route, and theNumber(“abc”)returnsNaN. The component that does not handle NaN` is the component that breaks.

The nested parameter problem. The /users/:userId/posts/:postId route has two parameters. The useParams hook returns both. The component reads userId and postId from the same object. The nested route’s parameter is also available in the parent route’s component if the child renders it via the <Outlet /> and the parent uses the useParams hook. The parameters are merged.

The optional parameter problem. The /posts/:year/:month? route matches both /posts/2026 and /posts/2026/10. The useParams hook returns { year: "2026", month: undefined } for the first URL and { year: "2026", month: "10" } for the second. The component must handle the undefined case. The month || "01" fallback is the common pattern.

The splat problem. The /files/* route matches any URL under /files/. The useParams hook returns the wildcard value under the "*" key. The value includes the slashes. The /files/a/b/c returns { "*": "a/b/c" }. The splat is the mechanism for the file browser, the breadcrumb trail, and the catch-all routes.

The validation problem. The route matches the URL, but the parameter value may not be what the component expects. The useParams returns the string, and the component must validate it. The zod library is the common choice: the z.object({ id: z.coerce.number() }) schema parses the string to a number and throws if the value is not numeric. The router’s params.parse option in the route definition is the alternative. The validation is the difference between a broken app and a graceful error.

The trade-off. The path parameter is the simplest way to pass data in the URL. The query parameter is the alternative for the optional data. The path parameter is for the resource identifier. The query parameter is for the filtering, the sorting, and the pagination. The correct choice depends on whether the data identifies the resource or configures the view.


a. The basic parameter and the useParams hook

The :id in the path is the parameter. The useParams hook returns the value.

import { createBrowserRouter, useParams } from "react-router";
import { RouterProvider } from "react-router/dom";

function UserProfile() {
  const { id } = useParams();
  return <h1>User: {id}</h1>;
}

const router = createBrowserRouter([
  {
    path: "/",
    Component: Root,
    children: [
      { path: "users/:id", Component: UserProfile },
    ],
  },
]);

The /users/123 URL matches the users/:id route. The useParams returns { id: "123" }. The component renders User: 123.

The parameter name is arbitrary. The :userId and :id are both valid. The name must match the key that the component reads from the useParams object.


b. The nested parameters and the optional parameter

The nested route path has multiple parameters. The useParams returns all of them.

function PostDetail() {
  const { userId, postId } = useParams();
  return (
    <div>
      <p>User: {userId}</p>
      <p>Post: {postId}</p>
    </div>
  );
}

const router = createBrowserRouter([
  {
    path: "users/:userId/posts/:postId",
    Component: PostDetail,
  },
]);

The /users/1/posts/42 URL returns { userId: "1", postId: "42" }. The component reads both.

The optional parameter uses the ? suffix. The :month? matches both the presence and the absence of the segment.

function PostsArchive() {
  const { year, month } = useParams();
  return (
    <div>
      <p>Year: {year}</p>
      <p>Month: {month || "all"}</p>
    </div>
  );
}

const router = createBrowserRouter([
  {
    path: "posts/:year/:month?",
    Component: PostsArchive,
  },
]);

The /posts/2026 returns { year: "2026", month: undefined }. The /posts/2026/10 returns { year: "2026", month: "10" }. The component uses the month || "all" fallback.


c. The splat and the validation

The splat parameter is the * wildcard. The useParams returns the value under the "*" key.

function FileBrowser() {
  const { "*": path } = useParams();
  return <p>Path: {path}</p>;
}

const router = createBrowserRouter([
  {
    path: "files/*",
    Component: FileBrowser,
  },
]);

The /files/a/b/c returns { "*": "a/b/c" }. The component reads the value and splits it on the slashes to build the breadcrumb.

The validation is the second step. The useParams returns the string, and the component must parse and validate it.

import { z } from "zod";

const paramsSchema = z.object({
  id: z.coerce.number().int().positive(),
});

function UserProfile() {
  const params = useParams();
  const result = paramsSchema.safeParse(params);

  if (!result.success) {
    return <p>Invalid user ID</p>;
  }

  const { id } = result.data;
  return <h1>User: {id}</h1>;
}

The z.coerce.number() converts the string to a number and throws if the value is not numeric. The safeParse returns a result object instead of throwing. The result.success check is the validation. The result.data.id is the parsed number.

The route-level params.parse is the alternative. The parse function is defined in the route object, and the router calls it before the component renders.

const router = createBrowserRouter([
  {
    path: "users/:id",
    Component: UserProfile,
    params: {
      parse: (params) => ({ id: Number(params.id) }),
    },
  },
]);

The parse function receives the raw string params and returns the parsed params. The component’s useParams returns the parsed values. The invalid value causes the route to throw, which the nearest error boundary catches.


Complete Example Session

# ============================================
# PART 1: BASIC PARAMETER
# ============================================

function UserProfile() {
  const { id } = useParams();
  return <h1>User: {id}</h1>;
}

{ path: "users/:id", Component: UserProfile }
# ============================================
# PART 2: NESTED PARAMETERS
# ============================================

function PostDetail() {
  const { userId, postId } = useParams();
  return <p>User {userId}, Post {postId}</p>;
}

{ path: "users/:userId/posts/:postId", Component: PostDetail }
# ============================================
# PART 3: OPTIONAL PARAMETER
# ============================================

function PostsArchive() {
  const { year, month } = useParams();
  return <p>Year {year}, Month {month || "all"}</p>;
}

{ path: "posts/:year/:month?", Component: PostsArchive }
# ============================================
# PART 4: SPLAT PARAMETER
# ============================================

function FileBrowser() {
  const { "*": path } = useParams();
  return <p>Path: {path}</p>;
}

{ path: "files/*", Component: FileBrowser }
# ============================================
# PART 5: STRING PROBLEM
# ============================================

const { id } = useParams();
// id is "123", not 123
console.log(typeof id); // "string"
# ============================================
# PART 6: VALIDATION WITH ZOD
# ============================================

const schema = z.object({ id: z.coerce.number() });
const result = schema.safeParse(useParams());
if (!result.success) return <p>Invalid ID</p>;
# ============================================
# PART 7: ROUTE PARAMS PARSE
# ============================================

{
  path: "users/:id",
  Component: UserProfile,
  params: { parse: (params) => ({ id: Number(params.id) }) },
}
# ============================================
# PART 8: PARENT PARAMS IN CHILD
# ============================================

function Child() {
  const { id } = useParams(); // available from parent
  return <p>Parent ID: {id}</p>;
}
# ============================================
# PART 9: VERIFY
# ============================================

# /users/123 → User 123
# /users/abc → Invalid ID
# /posts/2026 → Year 2026, Month all
# /files/a/b → Path: a/b

The nine parts trace the parameter flow: basic, nested, optional, splat, string problem, validation, route parse, parent params, and verification.


Quick Reference

Path Patterns

PatternExample URLParams
users/:id/users/1{ id: "1" }
users/:uid/posts/:pid/users/1/posts/2{ uid: "1", pid: "2" }
posts/:year/:month?/posts/2026{ year: "2026" }
files/*/files/a/b{ "*": "a/b" }

Hooks

HookReturns
useParams()Object of string params
useMatch(pattern)Match object
useLocation()Current location

Parsing Options

OptionLocation
params.parseRoute object
z.coerce.number()Component
Number(id)Component
parseInt(id, 10)Component

Validation

ApproachError Handling
Number(id)NaN
parseIntNaN
zod.safeParseresult.success
params.parseRoute throws to boundary

Best Practices

✅ Do This:

const { id } = useParams();
const numericId = Number(id);
<!-- Parse the parameter explicitly -->              # ✅
const result = schema.safeParse(useParams());
if (!result.success) return <Invalid />;
<!-- Validate before using -->                       # ✅
{ path: "users/:id", params: { parse: (p) => ({ id: Number(p.id) }) } }
<!-- Parse at the route level -->                    # ✅

❌ Don’t Do This:

const { id } = useParams();
array[id]
<!-- String index on array -->                      # ❌
fetch(`/api/users/${id}`).then(r => r.json()).then(data => array[data.id])
<!-- Unvalidated id can be "abc" -->                 # ❌
{ path: "users/:id?" }
<!-- Optional parameter without handling undefined --> # ❌

Common Pitfalls

PitfallWhy It HappensFix
Array index wrongString used as numberNumber(id)
API returns 404id is "abc"Validate before fetch
undefined crashOptional parameter absentCheck for undefined
Splat emptyRoute path wrongUse * at the end
Params not available in childParent renders without <Outlet />Add Outlet

Real-World Examples

1. Basic Param

const { id } = useParams();

2. Nested Params

const { userId, postId } = useParams();

3. Optional Param

const { year, month } = useParams();
const m = month || "01";

4. Splat Param

const { "*": path } = useParams();

5. Number Parse

const id = Number(useParams().id);

6. Zod Validation

z.object({ id: z.coerce.number() }).safeParse(useParams());

7. Route Parse

params: { parse: (p) => ({ id: Number(p.id) }) }

8. useMatch

const match = useMatch("/users/:id");

9. Invalid Handling

if (Number.isNaN(id)) return <Invalid />;

10. Parent Param

const { id } = useParams(); // parent's id

Visual

Parameter Flow

┌──────────────────────────────────────────────┐
│  URL: /users/123                             │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Route: /users/:id                           │
│  → Match: { id: "123" }                      │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Component: useParams()                      │
│  → { id: "123" } (string)                    │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Parse: Number("123") → 123                  │
│  Validate: isNaN(123)? No                    │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Render: <h1>User: 123</h1>                  │
└──────────────────────────────────────────────┘

String vs Number

┌──────────────────────────────────────────────┐
│  useParams() returns { id: "123" }           │
│  → typeof id === "string"                    │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  array["123"] → undefined                    │
│  array[123] → correct element                │
└──────────────────────────────────────────────┘

Summary

ItemValue
Path parameter:id in path
Access hookuseParams()
Return typeAlways string
Optional parameter:id?
Splat parameter*
ValidationNumber, parseInt, zod
Route-level parseparams.parse
Parent params in childAvailable

Key takeaways:

  • The parameter value is always a string. The useParams hook returns the string, not the number. The Number(id) or parseInt(id, 10) converts it. The array[id] with a string returns undefined. The array[Number(id)] returns the element.
  • The validation is the second step. The string "abc" matches the :id route, and the Number("abc") returns NaN. The component must check for NaN or use the zod schema with safeParse.
  • The optional parameter uses the ? suffix. The :month? matches both /posts/2026 and /posts/2026/10. The useParams returns undefined for the missing segment.
  • The splat uses the * wildcard. The useParams()["*"] returns the remainder of the URL, including the slashes. The splat is the mechanism for the file browser and the breadcrumbs.
  • The route-level params.parse is the alternative to the component validation. The parse function is called by the router, and the component receives the parsed value. The invalid value throws to the nearest error boundary.

Remember: The parameter is the string. The parse is the conversion. The validation is the check. The useParams is the access. The params.parse is the route-level. The exam tests the string problem and the validation.

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!