Skip to content
React rc routing 4 min read

Route Params & Query Strings

Most real applications need URLs that carry data: /users/42, /blog/hello-world, or /products?sort=price&page=2. React Router gives you two distinct tools for this. Dynamic path segments identify a resource and are read with useParams, while query strings hold optional, view-shaping state like filters and pagination and are read with useSearchParams. Knowing which one to reach for keeps your URLs clean, shareable, and bookmarkable.

Dynamic segments

A dynamic segment is a part of a route path prefixed with a colon. When the URL matches, React Router captures the actual value and hands it to the matched component. You declare the placeholder in the path and read it back by the same name.

import { BrowserRouter, Routes, Route } from 'react-router-dom';
import UserProfile from './pages/UserProfile';

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

export default App;

Here :id matches a single segment. Navigating to /users/42 renders UserProfile, and /users/abc matches it too — segments are matched as strings, so any non-empty value is accepted.

Reading params with useParams

Inside the matched component, useParams returns an object whose keys are the names you used in the path. Values are always strings (or undefined for optional params), so coerce them when you need a number.

import { useParams } from 'react-router-dom';

function UserProfile() {
  const { id } = useParams();
  const userId = Number(id);

  return (
    <section>
      <h1>User #{userId}</h1>
      <p>Loading profile for id {id}…</p>
    </section>
  );
}

Output:

// URL: /users/42
User #42
Loading profile for id 42…

A route can declare several dynamic segments, and they all appear on the same object.

<Route path="/orgs/:orgId/repos/:repoId" element={<Repo />} />
function Repo() {
  const { orgId, repoId } = useParams();
  return <h1>{orgId} / {repoId}</h1>;
}

Param values are URL-decoded for you, but never trust them blindly. Validate or parse the value and render a “not found” state when it does not resolve to a real resource.

Optional and splat params

Two special syntaxes extend basic segments. An optional param ends with ? and matches whether or not that segment is present. A splat (or catch-all) uses * to match the rest of the path, however many segments it contains.

<Routes>
  {/* Matches /products and /products/shoes */}
  <Route path="/products/:category?" element={<Products />} />

  {/* Matches /files/a, /files/a/b/c.txt, etc. */}
  <Route path="/files/*" element={<FileBrowser />} />
</Routes>

The splat value is exposed under the * key, which you read with bracket notation.

function FileBrowser() {
  const params = useParams();
  const path = params['*']; // "docs/2026/report.pdf"
  return <code>{path}</code>;
}

Query strings with useSearchParams

Path params answer which resource; query strings answer how to present it. Use them for filters, sorting, pagination, and search terms — anything optional that should survive a refresh or be sharable via a link. useSearchParams mirrors useState: it returns the current params and a setter, and updating it changes the URL.

import { useSearchParams } from 'react-router-dom';

function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();

  const sort = searchParams.get('sort') ?? 'name';
  const page = Number(searchParams.get('page') ?? '1');

  return (
    <div>
      <select
        value={sort}
        onChange={(e) =>
          setSearchParams({ sort: e.target.value, page: '1' })
        }
      >
        <option value="name">Name</option>
        <option value="price">Price</option>
      </select>

      <p>Sorting by {sort}, page {page}</p>

      <button onClick={() => setSearchParams({ sort, page: String(page + 1) })}>
        Next page
      </button>
    </div>
  );
}

Output:

// After selecting "Price" then clicking Next page twice:
// URL: /products?sort=price&page=3
Sorting by price, page 3

searchParams is a standard URLSearchParams instance, so get, getAll, and has all work. Passing an object or a URLSearchParams to setSearchParams replaces the whole query string, so spread the existing values when you want to update one key without dropping the rest.

setSearchParams((prev) => {
  prev.set('page', '2');
  return prev;
});

The functional updater receives the previous URLSearchParams, letting you mutate a single key safely. By default updates push a new history entry; pass { replace: true } as the second argument to update in place — handy for rapid filter changes you do not want to clutter the back button.

Params vs. query strings

AspectPath param (useParams)Query string (useSearchParams)
Declared in route?Yes, as :name in pathNo, free-form
Required?Usually requiredOptional
IdentifiesA specific resourceHow to view it
Typical use/users/:id, /posts/:slug?sort=, ?page=, ?q=
Read withuseParams()useSearchParams()
Update withNavigation (Link, navigate)setSearchParams

Best Practices

  • Use path params for the identity of a resource and query strings for optional, view-shaping state.
  • Always coerce param and query values from strings (Number, Boolean) and validate them before use.
  • Provide sensible defaults with ?? when reading query params so the UI works without them present.
  • When updating one query key, use the functional updater or spread existing params so you do not clobber the others.
  • Pass { replace: true } for high-frequency updates like live search to avoid polluting browser history.
  • Keep sharable, refresh-safe state in the URL instead of component state so links and bookmarks just work.
Last updated June 14, 2026
Was this helpful?