React Router 07 ⚛️ Path Parameters — Dynamic URL segments
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
| Pattern | Example URL | Params |
|---|---|---|
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
| Hook | Returns |
|---|---|
useParams() | Object of string params |
useMatch(pattern) | Match object |
useLocation() | Current location |
Parsing Options
| Option | Location |
|---|---|
params.parse | Route object |
z.coerce.number() | Component |
Number(id) | Component |
parseInt(id, 10) | Component |
Validation
| Approach | Error Handling |
|---|---|
Number(id) | NaN |
parseInt | NaN |
zod.safeParse | result.success |
params.parse | Route 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Array index wrong | String used as number | Number(id) |
| API returns 404 | id is "abc" | Validate before fetch |
undefined crash | Optional parameter absent | Check for undefined |
| Splat empty | Route path wrong | Use * at the end |
| Params not available in child | Parent 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
| Item | Value |
|---|---|
| Path parameter | :id in path |
| Access hook | useParams() |
| Return type | Always string |
| Optional parameter | :id? |
| Splat parameter | * |
| Validation | Number, parseInt, zod |
| Route-level parse | params.parse |
| Parent params in child | Available |
Key takeaways:
- The parameter value is always a string. The
useParamshook returns the string, not the number. TheNumber(id)orparseInt(id, 10)converts it. Thearray[id]with a string returnsundefined. Thearray[Number(id)]returns the element. - The validation is the second step. The string
"abc"matches the:idroute, and theNumber("abc")returnsNaN. The component must check forNaNor use thezodschema withsafeParse. - The optional parameter uses the
?suffix. The:month?matches both/posts/2026and/posts/2026/10. TheuseParamsreturnsundefinedfor the missing segment. - The splat uses the
*wildcard. TheuseParams()["*"]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.parseis 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.
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!