skip to content

Params and Search Params

useParams reads dynamic segment values and useSearchParams treats the query string as state you can get, set, and delete. Interviewers like search params because they force a conversation about the URL as the source of truth.

on this pageshow

explore

questions

4

In React Router v7, what type are the values useParams returns, and what must you do before using :productId as a number?

level: juniorimportance: must knowfreq 64%

answer

  1. the URL only holds text
  2. string or undefined
  3. the pattern does not validate
  4. parse, check, then handle invalid

basics

~20 s

useParams values are strings or undefined, decoded from the URL and never coerced. A :productId segment matches any text, so parse it, check the result, and render a not-found or error state when it is missing or invalid.

solid answer

~40 s

`useParams()` returns an object typed as `{ readonly [key: string]: string | undefined }`: every value is the percent-decoded text of its URL segment, never a number or a date. The route pattern `products/:productId` accepts any non-empty segment, so `/products/abc` matches and hands you `"abc"`. Before using it as a number, parse it (`Number(productId)`), check it (`Number.isInteger(id) && id > 0`) and branch on failure, typically to a not-found screen. `undefined` is part of the type because the hook is not tied to the route config: the component might render under a route without that param, and optional segments are genuinely absent. Child routes also see their parents' params, so a nested reviews screen can read `productId`.

code

tsx · 14 lines
tsx
import { useParams } from "react-router";

function useProductId(): number | null {
  const { productId } = useParams();
  if (!productId || !/^\d+$/.test(productId)) return null;
  const id = Number(productId);
  return id > 0 ? id : null;
}

export function ProductPage() {
  const productId = useProductId();
  if (productId === null) return <p role="alert">Product not found.</p>;
  return <ProductDetails id={productId} />;
}

go deeper

for a junior

Recall that useParams values are strings, possibly undefined, and that you must convert and check them before using them as numbers.

for a middle

Explain why the type includes undefined, that dynamic segments accept any text, that values are decoded, and that child routes inherit parent params.

for a senior

Centralise parsing and validation in a small hook, treat URL values as untrusted input, and render a deliberate not-found state for bad ids.

for a principal

Set conventions for URL-derived ids across teams, such as shared parsers and one not-found path, so invalid URLs fail the same way everywhere.

## What `useParams` returns `useParams()` returns the **dynamic segment values** of the current match, as an object keyed by the names used in the route pattern. For a route declared as `products/:productId` and the URL `/products/42`, it returns `{ productId: "42" }`. Three properties of those values drive everything else: - **They are strings.** A URL is text; React Router does not coerce `"42"` into a number or `"2026-09-25"` into a date. - **They are decoded.** Percent-encoding is removed, so `/products/red%20shoes` yields `"red shoes"`. - **They may be `undefined`.** The type is `Params = { readonly [key: string]: string | undefined }`. Child routes inherit their parents' params. A `reviews` route nested under `products/:productId` sees `productId` in its own `useParams()` result. A splat value appears under the key `"*"`. ## Why the pattern does not validate A dynamic segment in React Router matches **any non-empty segment** of the URL. `:productId` is a name, not a type, so all of these match `products/:productId`: | URL | `productId` | |---|---| | `/products/42` | `"42"` | | `/products/abc` | `"abc"` | | `/products/-1` | `"-1"` | | `/products/4.5` | `"4.5"` | Users edit URLs, links go stale, and crawlers try odd paths, so a component that assumes a well-formed number will eventually receive something else. ## Parse, check, branch The robust pattern has three steps: 1. **Parse** into the type you need: `Number(productId)` or a stricter parser. 2. **Check** the result against your domain's rules: `Number.isInteger(id) && id > 0`, a UUID format, a known slug list. 3. **Branch** on failure to a deliberate outcome: a not-found screen, a message, or a redirect to the list. Doing this once, near the top of the route's component (or in a small helper like `useProductId()`), keeps the rest of the tree working with a trusted value. ## Why `undefined` is in the type In declarative and data modes, `useParams` has no link to the route configuration. The hook cannot know that the component is only rendered under `products/:productId`, and in fact it may not be: - the component might be reused under a different route that has no `productId`; - optional segments (`:lang?`) are really absent when the URL omits them; - tests may render the component without the route. The generic form `useParams<"productId">()` narrows which **keys** exist but keeps each value `string | undefined`. The honest handling is a guard, not a non-null assertion: `if (!productId) return <NotFound />`. ## Common mistakes - **`Number(params.productId)` without a check.** `Number("abc")` is `NaN`, and `NaN` flows into requests and comparisons without an error. - **`parseInt` leniency.** `parseInt("42abc", 10)` returns `42`, silently accepting a malformed URL; prefer `Number` plus an integer check, or a regular expression. - **Non-null assertions everywhere** (`params.productId!`), which turn a missing param into a runtime failure far from its cause. - **Comparing strings to numbers.** `productId === 42` is always false; compare like with like. - **Treating the URL as trusted input.** Anything read from `useParams` is user-controlled and must be validated before it reaches an API call. ## The reverse direction: building URLs with params When you create a link to a product, the same rules apply in reverse. `generatePath("/products/:productId", { productId: String(id) })` fills the pattern and runs each dynamic value through `encodeURIComponent`, so a slug containing a space or a slash cannot break the path. It also fails fast: a missing required param throws an invariant error naming it (`Missing ":productId" param`), while optional ones may be omitted. Building the path by string concatenation skips both protections, which is how a value like `a/b` ends up split across two segments and matching a different route entirely. ## Summary - Values are decoded strings, possibly `undefined`, never coerced. - `:param` accepts any segment text; the route does not validate format. - Parse, check and branch once, near the route's entry component. - Child routes see their ancestors' params.

  • In React Router v7, a reviews route is nested under products/:productId. Can its component read productId?
    Yes. Child routes inherit all params from their parent routes, so `useParams()` in the reviews component returns `productId` alongside any params the reviews route declares itself. Avoid giving a child the same param name as an ancestor, because the later value overrides the earlier one.
  • In React Router v7, why not simply write useParams().productId! to satisfy TypeScript?
    The assertion hides a real case. If the component is rendered under a route without that param, or in a test without the route, the value is `undefined` and the failure surfaces later as a confusing error. A guard that renders a not-found state keeps the failure local and visible.

saying these in an interview costs you the question

  • useParams converts numeric segments into numbers automatically
  • A :productId segment only matches digits
  • The generic useParams<"productId">() removes undefined from the type
  • parseInt is enough validation for an id read from the URL
  • A child route cannot see its parent route's params
open as a page

In React Router v7, why does setSearchParams({ page: "2" }) drop ?category and ?sort, and how do you change one param while keeping the rest?

level: middleimportance: must knowfreq 62%

basics

~20 s

setSearchParams replaces the entire query string with what you pass, so an object with only page discards everything else. Use the callback form: take the copy it receives, set or delete the one key, and return it.

open as a page

In a React Router v7 product list, a category handler calls setSearchParams twice, once for category and once to reset page, and the category change is lost; why?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The setSearchParams callback does not queue like React's setState: both calls start from the params of the current render, and each starts a navigation. The second call's URL, which lacks the new category, wins. Make every change in one call.

open as a page