So far we have defined static routes: / is the Home, /about is About. A 1 to 1 relationship.
Now think about an online store with 10,000 products. Are we going to write 10,000 <Route> lines by hand? path="/product/shoe-1", path="/product/shoe-2"…
That’s a no. What we need is to define a Dynamic Route: a single route that can capture a variable value from the URL and use it to decide what to display.
Defining the parameter (:)
In React Router, to indicate that a part of the URL is a variable parameter, we use the colon : syntax.
When writing path="/products/:id", we are telling the Router:
Any URL starting with /products/ followed by something matches here. And store that ‘something’ in a variable called
id.
Let’s modify our main routes file:
// src/App.jsx
import { Routes, Route } from 'react-router';
import ProductList from './pages/ProductList';
import ProductDetail from './pages/ProductDetail';
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<ProductList />} />
{/* 👇 Here is the parameter: :id */}
<Route path="/products/:id" element={<ProductDetail />} />
</Routes>
);
}
This will match:
/products/123/products/iphone-15/products/abc-xyz
The useParams Hook
Now that the route captures the value, we need to access it from within the <ProductDetail /> component. For this, React Router offers us the hook useParams.
This hook returns an object with key/value pairs of all dynamic parameters from the current URL.
// src/pages/ProductDetail.jsx
import { useParams } from 'react-router';
export default function ProductDetail() {
// 1. Extract the "id" parameter (must match what's in the Route)
const { id } = useParams();
return (
<div>
<h1>You are viewing the product with ID: {id}</h1>
{/* Here we would use this ID to request data from an API */}
</div>
);
}
Real-world example: List and detail
Let’s see the complete flow. First, a list of links, and then the detail page that uses the ID.
The list
In the product list, we use the Link component, constructing the URL dynamically.
// src/pages/ProductList.jsx
import { Link } from 'react-router';
const products = [
{ id: 1, name: 'Gaming Laptop' },
{ id: 2, name: 'Mechanical Keyboard' },
{ id: 3, name: 'Wireless Mouse' }
];
export default function ProductList() {
return (
<div>
<h2>Catalog</h2>
<ul>
{products.map((prod) => (
<li key={prod.id}>
{/* Build the URL: /products/1, /products/2... */}
<Link to={`/products/${prod.id}`}>
View {prod.name}
</Link>
</li>
))}
</ul>
</div>
);
}
The detail
Now, in the detail component, we use that id to find the correct information. In a real app, we would make a fetch to an API, but here we’ll simulate a local database.
// src/pages/ProductDetail.jsx
import { useParams, Link } from 'react-router';
// Simulated "database"
const database = [
{ id: 1, name: 'Gaming Laptop', price: 1200, desc: 'Pure power' },
{ id: 2, name: 'Mechanical Keyboard', price: 80, desc: 'Clicky clicky' },
{ id: 3, name: 'Wireless Mouse', price: 40, desc: 'No cables' }
];
export default function ProductDetail() {
const { id } = useParams();
// ⚠️ IMPORTANT: useParams ALWAYS returns strings.
// If your IDs are numeric, you must convert them.
const product = database.find(item => item.id === Number(id));
// Handling "Not found" case
if (!product) {
return <h2>Product not found 😢</h2>;
}
return (
<div className="detail">
<h1>{product.name}</h1>
<p>Price: {product.price}€</p>
<p>Description: {product.desc}</p>
<Link to="/products">⬅ Back to list</Link>
</div>
);
}
URL parameters are always string. Even if the URL is /products/5, useParams will return { id: "5" }.
If you do item.id === id (strict comparison with a number), it will fail. Use Number(id) or parseInt(id).
