ReactRoute Params & Search Params

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:

TSX
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>
  )
}
Note
All path params from useParams() are strings. Convert to the correct type before using: const id = Number(productId), or validate with a schema library.
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:

TSX
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:

TSX
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>
  )
}
Tip
Always use the function form of setSearchParams((prev) => ...) when updating a single param. The plain object form discards all other params — a common source of bugs.
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:

TSX
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:

TSX
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-asc directly from email or docs.

  • No hydration mismatch — server and client see the same initial state from the URL.

Warning
Avoid duplicating URL state in useState. Pick one source of truth: either the URL (via useSearchParams) or component state (via useState). Reading the URL and syncing it into a separate useState causes bugs — the two sources get out of sync.
Reading Multiple Values from One Param

URLSearchParams.getAll() returns an array for params that appear multiple times (e.g. ?tag=react&tag=typescript):

TSX
// 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>
  )
}
Note
URLSearchParams is a browser-native API. You do not need a special library to parse query strings with React Router — useSearchParams() gives you the full URLSearchParams interface.