Full-project lesson · needs Node.js, npm and real project files, so it does not run in the in-page playground.
Many pages show one item out of many: /products/42, /users/ada, /articles/my-post. You do not write a route for each product. You write one route with a dynamic segment and read the value from the URL.
Dynamic segments and useParams
A segment that starts with : is a placeholder. useParams() returns an object with the actual values:
import { Routes, Route, NavLink, Link, useParams, useSearchParams } from 'react-router'
const PRODUCTS = [
{ id: 1, name: 'Keyboard' },
{ id: 2, name: 'Mouse' },
{ id: 3, name: 'Monitor' },
]
function Home() {
return <h1>Home page</h1>
}
function Products() {
const [searchParams] = useSearchParams()
const sort = searchParams.get('sort')
const list = sort === 'name' ? [...PRODUCTS].sort((a, b) => a.name.localeCompare(b.name)) : PRODUCTS
return (
<div>
<h1>Our products</h1>
<p>{sort === 'name' ? 'Sorted by name' : 'Default order'}</p>
<ul>
{list.map((p) => (
<li key={p.id}>
<Link to={`/products/${p.id}`}>{p.name}</Link>
</li>
))}
</ul>
</div>
)
}
function ProductDetail() {
const { id } = useParams() // always a string
const product = PRODUCTS.find((p) => p.id === Number(id))
if (!product) {
return (
<div>
<h1>We could not find product {id}</h1>
<Link to="/products">Back to the list</Link>
</div>
)
}
return (
<div>
<h1>Product {product.id}: {product.name}</h1>
<Link to="/products">Back to the list</Link>
</div>
)
}
function App() {
return (
<div>
<nav style={{ display: 'flex', gap: 16 }}>
<NavLink to="/" end>Home</NavLink>
<NavLink to="/products">Products</NavLink>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
</Routes>
</div>
)
}
export default App
Tested in a browser: the list links go to detail pages, a direct visit to /products/3 works, an unknown id shows a friendly message, and ?sort=name changes the list.
Points to remember:
:idbecomesparams.id. Name segments for what they are::productId,:slug,:username.- Values are always strings.
useParams()gives"2", not2. Convert withNumber(id), and decide what to do if the result isNaN. - Parameters can be wrong. A user can type any URL. Always handle “no such item” (see Not Found Pages), and never trust a parameter for security decisions; the server must check access.
- Several parameters work as expected:
/users/:userId/posts/:postIdgives{ userId, postId }.
Query strings: useSearchParams
A query string (?sort=name&page=2) stores optional settings: filters, sort order, page number, search text. React Router wraps the browser’s URLSearchParams:
const [searchParams, setSearchParams] = useSearchParams()
const page = Number(searchParams.get('page') ?? 1)
const q = searchParams.get('q') ?? ''
setSearchParams({ q: 'router', page: '1' }) // /products?q=router&page=1
setSearchParams((prev) => { // keep the other params
prev.set('page', String(page + 1))
return prev
})
Why use the URL for filters and paging instead of state?
- The user can refresh and keep the same view.
- The view can be bookmarked or shared.
- Back and Forward step through the changes.
Pair it with Search and Filtering and Pagination.
Path parameters or query parameters?
| Use a path parameter for | Use a query parameter for |
|---|---|
Which resource: /products/42 | How to show it: ?sort=price&page=2 |
| Required values | Optional values |
| Identity | Filters, search, paging |
Other helpful hooks
| Hook | Returns |
|---|---|
useLocation() | The current location: pathname, search, hash, state |
useMatch(pattern) | Whether the URL matches a pattern, with params |
useNavigate() | A function to navigate from code |
Key points
:namein the path anduseParams()read dynamic segments; the values are strings.- Always handle missing or invalid parameters.
- Keep filters, search and page numbers in the query string with
useSearchParams.