Route Params & Search Params
URLs are the most powerful piece of application state you have. They are bookmarkable, shareable, and survive page refreshes. React Router gives you two hooks to work with URL state: useParams for dynamic path segments and useSearchParams for query strings.
useParams — Path Parameters
Path parameters are dynamic segments defined with :name in your route. They identify a specific resource — a user, a post, a product:
import { useParams } from 'react-router-dom'
// Route: /products/:category/:productId
function ProductPage() {
const { category, productId } = useParams()
// For URL /products/electronics/42:
// category = 'electronics'
// productId = '42'
return (
<div>
<p>Category: {category}</p>
<p>Product ID: {productId}</p>
</div>
)
}useSearchParams — Query String
Search params sit after the ? in the URL: /products?page=2&sort=price&category=tech. They are ideal for filter, sort, and pagination state — anything that should survive a page refresh or be shareable as a link.
useSearchParams returns a [searchParams, setSearchParams] pair. searchParams is a URLSearchParams instance:
import { useSearchParams } from 'react-router-dom'
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams()
// Reading params — returns null if the key is absent
const page = Number(searchParams.get('page') ?? '1')
const sort = searchParams.get('sort') ?? 'newest'
const category = searchParams.get('category') ?? ''
return (
<div>
<p>Page {page}, sorted by {sort}</p>
{category && <p>Filtered: {category}</p>}
</div>
)
}Setting Search Params
Call setSearchParams with an object or a function. Passing a full object replaces the entire query string — merge the existing params first if you want to update a single key:
function SortSelector() {
const [searchParams, setSearchParams] = useSearchParams()
const handleSort = (newSort: string) => {
// Spread existing params, then override 'sort'
setSearchParams((prev) => {
prev.set('sort', newSort)
prev.set('page', '1') // reset to page 1 when sort changes
return prev
})
}
return (
<select
value={searchParams.get('sort') ?? 'newest'}
onChange={(e) => handleSort(e.target.value)}
>
<option value="newest">Newest</option>
<option value="price-asc">Price: Low to High</option>
<option value="price-desc">Price: High to Low</option>
<option value="rating">Top Rated</option>
</select>
)
}Complete Product Listing with URL State
Here is a product listing page where filters, sort order, and pagination all live in the URL. Every combination is bookmarkable and shareable:
import { useSearchParams, Link } from 'react-router-dom'
const CATEGORIES = ['All', 'Electronics', 'Clothing', 'Books', 'Home']
const SORT_OPTIONS = [
{ value: 'newest', label: 'Newest' },
{ value: 'price-asc', label: 'Price ↑' },
{ value: 'price-desc', label: 'Price ↓' },
{ value: 'rating', label: 'Top Rated' },
]
const PAGE_SIZE = 12
function ProductListing() {
const [searchParams, setSearchParams] = useSearchParams()
const category = searchParams.get('category') ?? 'All'
const sort = searchParams.get('sort') ?? 'newest'
const page = Number(searchParams.get('page') ?? '1')
const setParam = (key: string, value: string) =>
setSearchParams((prev) => {
prev.set(key, value)
if (key !== 'page') prev.set('page', '1') // reset pagination on filter change
return prev
})
return (
<div>
{/* Category filter */}
<div>
{CATEGORIES.map((cat) => (
<button
key={cat}
onClick={() => setParam('category', cat)}
style={{ fontWeight: category === cat ? 'bold' : 'normal' }}
>
{cat}
</button>
))}
</div>
{/* Sort selector */}
<select value={sort} onChange={(e) => setParam('sort', e.target.value)}>
{SORT_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
{/* URL is now: /products?category=Electronics&sort=price-asc&page=1 */}
<p>
Showing {category} products, sorted by {sort}, page {page}
</p>
{/* Pagination */}
<div>
<button
disabled={page <= 1}
onClick={() => setParam('page', String(page - 1))}
>
Previous
</button>
<span>Page {page}</span>
<button onClick={() => setParam('page', String(page + 1))}>
Next
</button>
</div>
</div>
)
}Preserving Params When Navigating
When you navigate to a detail page and come back, users expect their filters to be intact. Pass the current search string in the Link or store it in navigation state:
import { Link, useLocation } from 'react-router-dom'
function ProductCard({ product }) {
const location = useLocation()
return (
<Link
to={`/products/${product.id}`}
state={{ from: location.search }} // preserve filter state
>
{product.name}
</Link>
)
}
// On the detail page, link back with the preserved search
function ProductDetail() {
const location = useLocation()
const fromSearch = location.state?.from ?? ''
return (
<div>
<Link to={`/products${fromSearch}`}>← Back to results</Link>
{/* product detail content */}
</div>
)
}Why URL State Beats Component State for Search
Bookmarkable — users can save or share a filtered/sorted/paginated view.
Back-button aware — browser history tracks each filter change separately.
Refresh-safe — the UI reconstructs exactly from the URL on reload.
Link-friendly — you can link to
?category=Electronics&sort=price-ascdirectly from email or docs.No hydration mismatch — server and client see the same initial state from the URL.
Reading Multiple Values from One Param
URLSearchParams.getAll() returns an array for params that appear multiple times (e.g. ?tag=react&tag=typescript):
// URL: /posts?tag=react&tag=typescript&tag=hooks
function TagFilter() {
const [searchParams, setSearchParams] = useSearchParams()
const activeTags = searchParams.getAll('tag') // ['react', 'typescript', 'hooks']
const toggleTag = (tag: string) => {
setSearchParams((prev) => {
const tags = prev.getAll('tag')
prev.delete('tag')
const next = tags.includes(tag) ? tags.filter((t) => t !== tag) : [...tags, tag]
next.forEach((t) => prev.append('tag', t))
return prev
})
}
return (
<div>
{['react', 'typescript', 'hooks', 'nextjs'].map((tag) => (
<button
key={tag}
onClick={() => toggleTag(tag)}
style={{ background: activeTags.includes(tag) ? 'tomato' : 'transparent' }}
>
{tag}
</button>
))}
</div>
)
}