URL Parameters

URL parameters let a single route respond to many different URLs by treating part of the path as a variable — for example, /users/42 and /users/99 both match the pattern /users/:userId, but each one carries a different userId. In React Router, you declare these dynamic segments in a route’s path and read their values inside your component with the useParams hook. This is the mechanism behind almost every detail page in a real app: product pages, user profiles, blog posts, and more.

Overview / How it works

A normal <Route> matches an exact path. A dynamic segment — written as a colon followed by a name, like :userId — matches any value in that position of the URL and captures it. When React Router matches the current URL against your route tree, it builds a params object from every dynamic segment in the matched path, and makes that object available to the matched component (and any of its descendants) through useParams().

It helps to separate two ideas that are easy to conflate: the path pattern you register with <Route path="/users/:userId">, and the actual URL the browser is currently showing, like /users/42. React Router compares the pattern to the current location on every navigation. If a segment in the pattern starts with :, it isn’t compared literally — instead, whatever text occupies that slot in the real URL is captured under that name. Everything captured this way arrives as strings, never numbers or booleans, because URLs are text. Converting "42" to the number 42 is your job.

Because routing state (the URL) lives outside your component tree, changing a param doesn’t remount your component the way changing a key would. If the user navigates from /products/1 to /products/2 and both match the same <Route path="/products/:productId">, React Router reuses the same component instance and just re-renders it with a new value from useParams(). This matters enormously for data fetching, which the Under the Hood and Common Mistakes sections below cover in detail.

Syntax

import { Routes, Route, useParams } from "react-router-dom";

<Routes>
  <Route path="/segment/:paramName" element={<Component />} />
</Routes>

function Component() {
  const { paramName } = useParams();
  // paramName is always a string (or undefined for optional segments)
}
  • :paramName — a dynamic segment in the path prop; matches one URL segment (no slashes) and captures its text.
  • useParams() — a hook that returns an object whose keys are the param names from the matched route’s path, and whose values are the matching strings from the current URL.
  • Multiple params — a path can declare more than one, e.g. path="/blog/:category/:postId" produces { category, postId }.
  • Optional segments — appending ? to a segment, e.g. path="/events/:year/:month?", makes that segment optional; if it’s absent from the URL, its value in the params object is undefined.

Examples

Example 1: A single param — user profile

import { BrowserRouter, Routes, Route, useParams } from "react-router-dom";

function UserProfile() {
  const { userId } = useParams();
  return (
    <div>
      <h2>User Profile</h2>
      <p>Viewing user with ID: {userId}</p>
    </div>
  );
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/users/:userId" element={<UserProfile />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Visiting /users/42 renders the heading “User Profile” and the text “Viewing user with ID: 42”. The :userId segment in the path matches 42, and useParams() hands that string back to the component. Notice there’s no manual URL parsing — React Router does the matching and extraction for you.

Example 2: Multiple params with generated links

import { BrowserRouter, Routes, Route, Link, useParams } from "react-router-dom";

function PostList() {
  const posts = [
    { category: "react", postId: "101", title: "Intro to Hooks" },
    { category: "css", postId: "202", title: "Flexbox Basics" },
  ];

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.postId}>
          <Link to={`/blog/${post.category}/${post.postId}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  );
}

function PostDetail() {
  const { category, postId } = useParams();
  return (
    <div>
      <h2>Post Detail</h2>
      <p>Category: {category}</p>
      <p>Post ID: {postId}</p>
    </div>
  );
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/blog" element={<PostList />} />
        <Route path="/blog/:category/:postId" element={<PostDetail />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

/blog renders a list of links built from data, each pointing to a URL with two dynamic segments filled in. Clicking “Intro to Hooks” navigates to /blog/react/101, which matches path="/blog/:category/:postId" and renders “Category: react” and “Post ID: 101”. This shows params working both directions: generating URLs from data with Link, and reading them back with useParams.

Example 3: Fetching data based on a param

import { useState, useEffect } from "react";
import { BrowserRouter, Routes, Route, useParams } from "react-router-dom";

function ProductDetail() {
  const { productId } = useParams();
  const [product, setProduct] = useState(null);
  const [status, setStatus] = useState("loading");

  useEffect(() => {
    setStatus("loading");
    fetch(`/api/products/${productId}`)
      .then((res) => {
        if (!res.ok) throw new Error("Not found");
        return res.json();
      })
      .then((data) => {
        setProduct(data);
        setStatus("success");
      })
      .catch(() => setStatus("error"));
  }, [productId]);

  if (status === "loading") return <p>Loading product {productId}...</p>;
  if (status === "error") return <p>Product not found.</p>;

  return (
    <div>
      <h2>{product.name}</h2>
      <p>Price: ${product.price}</p>
    </div>
  );
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/products/:productId" element={<ProductDetail />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Navigating between /products/1 and /products/2 re-runs the effect each time, because productId is listed in the dependency array — first showing “Loading product 2…” and then the new product’s name and price. Visiting an id that doesn’t exist on the server shows “Product not found.” This is the realistic pattern: pull the id out of the URL, then use it to fetch the matching data, re-fetching whenever the id changes.

How it works step by step / Under the hood

On initial load / mount: React Router parses the current URL, walks the route tree comparing each path pattern against it, and finds the best match. For every :name segment in the winning pattern, it records the corresponding piece of the URL into a params object. That object is stored in routing context. When your matched component renders and calls useParams(), it reads that object out of context.

On navigation to a URL that still matches the same route pattern: for example clicking a Link from /products/1 to /products/2 — React Router recognizes both URLs match the exact same <Route> element. Rather than unmounting ProductDetail and mounting a new one, it keeps the same component instance alive and simply re-renders it with an updated params object. Your component’s local state (from useState) is preserved across this transition — it does not reset automatically. Any useEffect that depends on the param value re-runs because its dependency array changed, which is exactly how the fetch in Example 3 refreshes.

On navigation to a URL matching a different route: the previously matched component unmounts (cleanup functions in its effects run), and the new matched component mounts fresh, with its own params object.

Common Mistakes

Mistake 1: Treating a param as a number

function UserProfile() {
  const { userId } = useParams();
  if (userId === 42) {
    return <p>Special user!</p>;
  }
  return <p>User {userId}</p>;
}

This condition is always false. useParams() always returns strings, so userId is "42", and "42" === 42 is false in JavaScript. Convert before comparing:

function UserProfile() {
  const { userId } = useParams();
  if (Number(userId) === 42) {
    return <p>Special user!</p>;
  }
  return <p>User {userId}</p>;
}

Mistake 2: Param name mismatch between route and component

// Route is declared as: <Route path="/users/:id" element={<UserProfile />} />
function UserProfile() {
  const { userId } = useParams(); // undefined — the path uses ":id", not ":userId"
  return <p>User: {userId}</p>;
}

Nothing throws an error here; you just silently get undefined, which is harder to debug. The key you destructure from useParams() must exactly match the name used after the colon in path:

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

Mistake 3: Omitting the param from a `useEffect` dependency array

useEffect(() => {
  fetch(`/api/products/${productId}`)
    .then((res) => res.json())
    .then(setProduct);
}, []); // missing productId — fetch only runs once, ever

Because React Router reuses the component instance when navigating between /products/1 and /products/2 (they match the same route), an empty dependency array means this effect never runs again after the first mount — the page silently keeps showing the first product. Always include every param the effect reads, as shown in Example 3’s [productId] array.

Best Practices

  • Always convert param strings with Number(), parseInt(), or similar before using them in numeric comparisons or math.
  • Keep the name after the colon in path identical to the key you destructure from useParams() — a typo here fails silently.
  • Include every param your useEffect reads in its dependency array so data refetches correctly when the user navigates between sibling URLs matching the same route.
  • Guard against invalid or missing param values (empty string, NaN, undefined) and render a clear fallback instead of letting the component crash.
  • Build links with <Link to={...}> using template literals rather than hand-written <a href> strings, so navigation stays client-side and params are inserted consistently.
  • Give params descriptive names (:productId, :reviewId) rather than generic ones (:id) when a route nests multiple identifiers, so useParams() results stay unambiguous.
  • Reserve URL params for identifying a specific resource (which user, which post); use useSearchParams for optional, filter-like state such as sorting or pagination.

Practice Exercises

  • Create a route /movies/:movieId whose component renders “Movie #” followed by the id. On a separate /movies page, render at least three Link elements pointing to different movie ids, each with a proper key.
  • Extend the route to /movies/:movieId/reviews/:reviewId and render a component that displays both the movie id and the review id side by side.
  • Add a guard to the movie component: if movieId converted to a number is not a positive integer, render “Invalid movie ID” instead of attempting to use it.

Summary

  • Dynamic segments in a route’s path, written as :name, capture part of the URL as a named parameter.
  • useParams() returns an object of all captured param values for the currently matched route, always as strings.
  • A path can declare multiple params, and segments can be marked optional with a trailing ?.
  • Navigating between URLs that match the same route reuses the component instance and re-renders it with new params — it does not remount, so effects need the param in their dependency array to react to changes.
  • Always convert param strings to the type you actually need, and validate them before use.