| |

React 60 ⚛️ Dynamic Route Parameters and Search Params

⏱️ Estimated read: 17 min | |

A URL carries two kinds of information. The path identifies a resource: /teams/firebirds names a specific team. The query string carries options: /teams/firebirds?tab=roster&sort=name names the same team but asks for a particular view of it. React Router exposes the first through useParams and the second through useSearchParams. The two are separate hooks because they serve separate purposes, and treating them as interchangeable is the source of most routing bugs. The path is the identity of the resource, and the query string is the configuration of the view.

Key point: Route parameters are part of the URL path and identify which resource is being shown. Search parameters are part of the query string and identify how the resource is shown. A change to the path means a different resource. A change to the query string means a different view of the same resource.


Why the distinction matters

The identity problem. A route param is a value captured from a path segment. /teams/:teamId captures firebirds from /teams/firebirds. The value is read-only in the sense that the component cannot change it directly; only navigation changes it. The param identifies the resource.

The configuration problem. A search param is a key-value pair in the query string. ?tab=roster means the page should show the roster tab. The component can change the search params without navigating to a different route, because the route is the same; only the view configuration changes.

The shareability problem. Both kinds of information are in the URL, which means both are shareable and bookmarkable. A user who copies /teams/firebirds?tab=roster and sends it to a colleague expects the colleague to see the same page with the same tab open. The URL is the complete state of the view.

The history problem. Each navigation, whether it changes the path or the query string, adds an entry to the browser’s history. The back button returns to the previous URL, which restores the previous view. This works because the URL carries all the state that the router needs.

The validation problem. URL values are strings, and they are user-controlled. A teamId from the URL must be validated against the list of real teams. A page number from the query string must be parsed and bounds-checked. The URL is untrusted input, and the component must treat it as such.

The server problem. The query string is sent to the server on the initial request, but the client-side router parses it on navigation. The server does not need to handle it unless the application uses server-side rendering. For a purely client-side application, the query string is the router’s responsibility.


a. Dynamic route parameters

A dynamic route parameter is a path segment that starts with :. The segment matches any value in that position, and the value is captured and made available to the route’s element.

The route definition

<Route path="teams/:teamId" element={<TeamDetail />} />

The :teamId segment matches any value. The URL /teams/firebirds matches with teamId equal to "firebirds". The URL /teams/dolphins matches with teamId equal to "dolphins".

The useParams hook

The useParams hook returns an object with the captured values.

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

function TeamDetail() {
  const { teamId } = useParams();
  return <h1>Team: {teamId}</h1>;
}

The keys of the object are the parameter names from the path. The values are the decoded URL segments.

The multiple parameters

A path can capture multiple parameters.

<Route path="teams/:teamId/players/:playerId" element={<PlayerDetail />} />
function PlayerDetail() {
  const { teamId, playerId } = useParams();
  return <h1>{playerId} plays for {teamId}</h1>;
}

The URL /teams/firebirds/players/42 captures teamId="firebirds" and playerId="42".

The optional parameters

A parameter can be optional by appending ? to the segment.

<Route path="teams/:teamId/:tab?" element={<TeamDetail />} />

The URL /teams/firebirds matches with teamId="firebirds" and no tab. The URL /teams/firebirds/roster matches with tab="roster".

The splat parameter

A splat captures the remaining path segments.

<Route path="files/*" element={<FileViewer />} />
function FileViewer() {
  const { '*': splat } = useParams();
  return <p>File: {splat}</p>;
}

The URL /files/docs/report.pdf captures splat="docs/report.pdf".

The parameter types

Every parameter is a string. A numeric ID must be parsed before use.

function PlayerDetail() {
  const { playerId } = useParams();
  const id = Number(playerId);
  if (Number.isNaN(id)) return <NotFound />;
  return <Player id={id} />;
}

The parsing and validation are the component’s responsibility. The router does not enforce types.

The parent parameters

A child route can read its parent’s parameters. The useParams hook returns all parameters from the matched route chain, merged into one object.

<Route path="teams/:teamId" element={<TeamsLayout />}>
  <Route path="players/:playerId" element={<PlayerDetail />} />
</Route>
function PlayerDetail() {
  const { teamId, playerId } = useParams();
  // Both are available.
}

The child route sees the parent’s teamId and its own playerId.

The parameter change

When the parameter changes, React Router re-renders the route’s element. The component does not remount; it re-renders with the new params. This matters for effects that depend on the parameter.

function TeamDetail() {
  const { teamId } = useParams();
  useEffect(() => {
    fetchTeam(teamId);
  }, [teamId]);
  // The effect reruns when teamId changes.
}

If the effect did not depend on teamId, it would not rerun, and the component would show stale data.


b. Search parameters

Search parameters are the key-value pairs in the query string. React Router exposes them through the useSearchParams hook, which returns a URLSearchParams object and a setter.

The useSearchParams hook

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

function TeamDetail() {
  const [searchParams, setSearchParams] = useSearchParams();
  const tab = searchParams.get('tab') ?? 'overview';
  return <div>Showing: {tab}</div>;
}

The searchParams object has the methods of URLSearchParams: get, getAll, has, entries, keys, and values.

The get method

get returns the first value for a key, or null if the key is absent.

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

The ?? operator provides a default when the key is absent.

The getAll method

getAll returns all values for a key, which is useful for repeated parameters.

// URL: ?tag=react&tag=router
const tags = searchParams.getAll('tag');  // ['react', 'router']

The has method

has returns a boolean indicating whether the key is present.

if (searchParams.has('debug')) {
  // Enable debug mode.
}

The set method

The setSearchParams function updates the query string. It accepts a URLSearchParams object, a string, or an object.

function setTab(tab) {
  setSearchParams({ tab });
}

The call updates the URL and triggers a re-render. The route does not change, because the path is the same.

The merge behavior

The setSearchParams function replaces the entire query string by default. To merge with the existing parameters, the setter accepts a function that receives the current params.

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

The function receives the current URLSearchParams object, modifies it, and returns it. The other parameters are preserved.

The delete method

The delete method removes a key from the query string.

setSearchParams((prev) => {
  prev.delete('debug');
  return prev;
});

The replace option

The setter accepts an options object with a replace flag. When replace: true, the navigation replaces the current history entry instead of adding a new one.

setSearchParams({ tab: 'roster' }, { replace: true });

This is useful for filter changes that should not clutter the history.

The array and object form

The setter accepts a plain object or an array of pairs.

setSearchParams({ tab: 'roster', sort: 'name' });
setSearchParams([['tab', 'roster'], ['sort', 'name']]);

The object form is the most readable for the common case.

The validation

Search parameters are strings and are user-controlled. A page parameter must be parsed and bounds-checked.

function parsePage(value) {
  const n = Number(value);
  if (!Number.isInteger(n) || n < 1) return 1;
  return n;
}

const page = parsePage(searchParams.get('page'));

The component treats the parameter as untrusted input and validates it before use.


c. When to use which

The choice between a route parameter and a search parameter is a design decision. The rule is simple: the path identifies the resource, and the query string configures the view.

The resource identity

A value that identifies a specific resource belongs in the path.

ValueLocationReason
Team IDPathIdentifies the team
Player IDPathIdentifies the player
User IDPathIdentifies the user
Document IDPathIdentifies the document

The view configuration

A value that configures how the resource is displayed belongs in the query string.

ValueLocationReason
TabQuerySelects the view
Page numberQueryPagination
Sort orderQueryOrdering
FilterQuerySubset
Search termQueryQuery

The state preservation

A value that should be preserved across navigation to a related resource belongs in the query string. A user who filters a list and then clicks into a detail page expects the filter to be remembered when they return.

The server consideration

The query string is sent to the server on the initial request. A search term in the query string is available to the server for server-side rendering. A path parameter is also available, but only as part of the path.

The security consideration

Both the path and the query string are user-controlled. Neither should be trusted. The component must validate every value it reads, whether it comes from useParams or useSearchParams.

The default value

A search parameter that is absent should have a default. The component uses the default when the parameter is missing, so the URL can be shorter.

const tab = searchParams.get('tab') ?? 'overview';
const page = Number(searchParams.get('page') ?? '1');

The URL ?tab=overview&page=1 and the URL with no query string produce the same view. The default is the canonical form.

The canonical URL

When the component updates a search parameter, it should update the URL to match. This keeps the URL in sync with the view, so a copied URL shows the same view.

function setTab(tab) {
  setSearchParams({ tab }, { replace: true });
}

The replace: true option keeps the history clean for filter changes that are not navigations.


Complete Example Session

// ============================================
// PART 1: THE ROUTE WITH A PARAMETER
// ============================================

<Route path="teams/:teamId" element={<TeamDetail />} />

// ============================================
// PART 2: READING THE PARAMETER
// ============================================

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

function TeamDetail() {
  const { teamId } = useParams();
  return <h1>Team: {teamId}</h1>;
}

// ============================================
// PART 3: MULTIPLE PARAMETERS
// ============================================

<Route path="teams/:teamId/players/:playerId" element={<PlayerDetail />} />

function PlayerDetail() {
  const { teamId, playerId } = useParams();
  return <h1>Player {playerId} on {teamId}</h1>;
}

// ============================================
// PART 4: THE SPLAT PARAMETER
// ============================================

<Route path="files/*" element={<FileViewer />} />

function FileViewer() {
  const { '*': splat } = useParams();
  return <p>File: {splat}</p>;
}

// ============================================
// PART 5: READING SEARCH PARAMS
// ============================================

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

function TeamDetailWithTabs() {
  const { teamId } = useParams();
  const [searchParams] = useSearchParams();
  const tab = searchParams.get('tab') ?? 'overview';
  return (
    <div>
      <h1>Team: {teamId}</h1>
      <p>Tab: {tab}</p>
    </div>
  );
}

// ============================================
// PART 6: UPDATING SEARCH PARAMS
// ============================================

function TeamTabs() {
  const [searchParams, setSearchParams] = useSearchParams();
  const tab = searchParams.get('tab') ?? 'overview';

  function selectTab(next) {
    setSearchParams({ tab: next });
  }

  return (
    <nav>
      <button onClick={() => selectTab('overview')}>Overview</button>
      <button onClick={() => selectTab('roster')}>Roster</button>
      <button onClick={() => selectTab('stats')}>Stats</button>
    </nav>
  );
}

// ============================================
// PART 7: MERGING SEARCH PARAMS
// ============================================

function FilteredList() {
  const [searchParams, setSearchParams] = useSearchParams();
  const page = Number(searchParams.get('page') ?? '1');
  const sort = searchParams.get('sort') ?? 'name';

  function setPage(next) {
    setSearchParams((prev) => {
      prev.set('page', String(next));
      return prev;
    });
  }

  return (
    <div>
      <p>Page {page}, sorted by {sort}</p>
      <button onClick={() => setPage(page + 1)}>Next</button>
    </div>
  );
}

// ============================================
// PART 8: VALIDATING PARAMETERS
// ============================================

function parsePage(value) {
  const n = Number(value);
  if (!Number.isInteger(n) || n < 1) return 1;
  return n;
}

function SafeList() {
  const [searchParams] = useSearchParams();
  const page = parsePage(searchParams.get('page'));
  return <p>Page {page}</p>;
}

// ============================================
// PART 9: THE REPLACE OPTION
// ============================================

function SearchBar() {
  const [searchParams, setSearchParams] = useSearchParams();
  const query = searchParams.get('q') ?? '';

  function handleChange(e) {
    setSearchParams({ q: e.target.value }, { replace: true });
  }

  return <input value={query} onChange={handleChange} />;
}

// Each keystroke replaces the history entry.
// The back button does not step through every character.

// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================

function TeamPage() {
  const { teamId } = useParams();
  const [searchParams, setSearchParams] = useSearchParams();
  const tab = searchParams.get('tab') ?? 'overview';
  const page = parsePage(searchParams.get('page'));

  function selectTab(next) {
    setSearchParams({ tab: next }, { replace: true });
  }

  return (
    <div>
      <h1>Team: {teamId}</h1>
      <nav>
        <button onClick={() => selectTab('overview')}>Overview</button>
        <button onClick={() => selectTab('roster')}>Roster</button>
      </nav>
      {tab === 'overview' && <Overview teamId={teamId} />}
      {tab === 'roster' && <Roster teamId={teamId} page={page} />}
    </div>
  );
}

// The path identifies the team.
// The query string configures the view.
// The URL is the complete state.

The ten parts showed the route with a parameter, reading the parameter, multiple parameters, the splat parameter, reading search params, updating search params, merging search params, validating parameters, the replace option, and the complete pattern.


Quick Reference

useParams

AspectValue
ReturnsObject of captured path parameters
ValuesStrings
Includes parent paramsYes
Splat key'*'
Re-renders on changeYes

useSearchParams

MethodPurpose
searchParams.get(key)First value or null
searchParams.getAll(key)All values
searchParams.has(key)Presence check
setSearchParams(obj)Replace the query string
setSearchParams(fn)Merge with current
setSearchParams(obj, { replace: true })Replace history entry

Path vs Query

ValueLocationReason
Resource IDPathIdentifies the resource
TabQueryConfigures the view
PageQueryPagination
SortQueryOrdering
FilterQuerySubset
Search termQueryQuery

Parameter Types

SyntaxMeaning
:idRequired parameter
:id?Optional parameter
*Splat (rest of path)

Navigation Options

OptionEffect
replace: trueReplace history entry
state: {...}Pass state
preventScrollResetKeep scroll position

Best Practices

✅ Do This:

// Use useParams for resource identity.
const { teamId } = useParams();  // ✅

// Use useSearchParams for view configuration.
const [searchParams] = useSearchParams();  // ✅

// Validate every URL value.
const page = parsePage(searchParams.get('page'));  // ✅

// Provide defaults for absent params.
const tab = searchParams.get('tab') ?? 'overview';  // ✅

// Merge when updating a single param.
setSearchParams((prev) => { prev.set('page', '2'); return prev; });  // ✅

// Use replace for filter changes.
setSearchParams({ q }, { replace: true });  // ✅

// Depend on params in effects.
useEffect(() => { fetchTeam(teamId); }, [teamId]);  // ✅

❌ Don’t Do This:

// Don't put view configuration in the path.
<Route path="teams/:teamId/:tab" />  // ❌ Tab is view config

// Don't trust URL values.
const page = Number(searchParams.get('page'));  // ❌ No validation

// Don't replace the entire query string when merging.
setSearchParams({ page: '2' });  // ❌ Drops other params

// Don't forget the dependency array.
useEffect(() => { fetchTeam(teamId); }, []);  // ❌ Stale on param change

// Don't use search params for resource identity.
// /teams?id=firebirds  // ❌ ID belongs in the path

// Don't ignore the replace option for filters.
// Each keystroke adds a history entry.  // ❌

Common Pitfalls

PitfallWhy It HappensFix
Param is undefinedRoute not matchedCheck the route definition
Value is a stringURL values are stringsParse and validate
Effect shows stale dataMissing teamId in depsAdd it to the dependency array
Query string replacedsetSearchParams with an objectUse the function form
History clutteredNo replace: trueAdd the option for filters
Parent param missingChild route defined separatelyDefine the child inside the parent
Splat is undefinedRoute has no *Add the splat to the path

Real-World Examples

1. Route Parameter

<Route path="teams/:teamId" element={<TeamDetail />} />

2. Read the Parameter

const { teamId } = useParams();

3. Multiple Parameters

const { teamId, playerId } = useParams();

4. Optional Parameter

<Route path="teams/:teamId/:tab?" element={<Team />} />

5. Splat Parameter

const { '*': splat } = useParams();

6. Read Search Param

const [searchParams] = useSearchParams();
const tab = searchParams.get('tab') ?? 'overview';

7. Update Search Param

setSearchParams({ tab: 'roster' });

8. Merge Search Params

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

9. Replace History

setSearchParams({ q }, { replace: true });

10. Validate Param

const page = parsePage(searchParams.get('page'));

Visual

Path vs Query

┌────────────────────────────────────────────────────────────┐
│  URL ANATOMY                                               │
│                                                            │
│  https://app.example.com/teams/firebirds?tab=roster&page=2 │
│  ─────────┬───────────── ─────┬─────── ─────────┬───────── │
│           │                   │                 │          │
│           │                   │                 │          │
│           │                   │                 └─ QUERY   │
│           │                   │                    tab,    │
│           │                   │                    page    │
│           │                   │                            │
│           │                   └─ PATH PARAM               │
│           │                      teamId = "firebirds"      │
│           │                                                │
│           └─ ORIGIN                                        │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  Path:  which resource?  → useParams              │     │
│  │  Query: how to view it?  → useSearchParams        │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

useParams Flow

┌────────────────────────────────────────────────────────────┐
│  useParams                                                 │
│                                                            │
│  Route: teams/:teamId/players/:playerId                    │
│  URL:   /teams/firebirds/players/42                        │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  useParams() returns:                             │     │
│  │  {                                                │     │
│  │    teamId: 'firebirds',                           │     │
│  │    playerId: '42'                                 │     │
│  │  }                                                │     │
│  │                                                   │     │
│  │  All values are strings.                          │     │
│  │  Parent params are included.                      │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  Validate:                                                │
│    const id = Number(playerId);                           │
│    if (Number.isNaN(id)) return <NotFound />;             │
└────────────────────────────────────────────────────────────┘

useSearchParams Flow

┌────────────────────────────────────────────────────────────┐
│  useSearchParams                                           │
│                                                            │
│  URL: /teams/firebirds?tab=roster&page=2                   │
│                                                            │
│  const [searchParams, setSearchParams] = useSearchParams() │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  searchParams.get('tab')   → 'roster'             │     │
│  │  searchParams.get('page')  → '2'                  │     │
│  │  searchParams.get('sort')  → null                 │     │
│  │                                                   │     │
│  │  Defaults:                                        │     │
│  │    const tab = searchParams.get('tab') ?? 'overview'│   │
│  │    const page = Number(searchParams.get('page') ?? '1')│
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  Update:                                                  │
│    setSearchParams({ tab: 'stats' })                      │
│    → /teams/firebirds?tab=stats (other params dropped)    │
│                                                            │
│  Merge:                                                   │
│    setSearchParams((prev) => { prev.set('tab', 'stats'); return prev })│
│    → /teams/firebirds?tab=stats&page=2                    │
└────────────────────────────────────────────────────────────┘

Path vs Query Decision

┌────────────────────────────────────────────────────────────┐
│  DECISION GUIDE                                            │
│                                                            │
│  Is the value the identity of a resource?                  │
│    YES → Path parameter                                    │
│    NO  → Continue                                          │
│                                                            │
│  Is the value a configuration of the view?                 │
│    YES → Search parameter                                  │
│    NO  → Continue                                          │
│                                                            │
│  Should the value be preserved across navigation?          │
│    YES → Search parameter                                  │
│    NO  → Continue                                          │
│                                                            │
│  Is the value sensitive?                                   │
│    YES → Consider not putting it in the URL at all         │
│    NO  → Path or query, based on the above                 │
└────────────────────────────────────────────────────────────┘

Summary

ItemValue
useParamsRead path parameters
useSearchParamsRead and write query string
Path parameterIdentifies a resource
Search parameterConfigures the view
:idRequired parameter
:id?Optional parameter
*Splat (rest of path)
getFirst value or null
getAllAll values for a key
setSearchParamsUpdate the query string
replace: trueReplace history entry

Key takeaways:

  • Path parameters identify resources, and search parameters configure the view. The path /teams/firebirds names a team. The query ?tab=roster asks for the roster tab of that team. The two are separate concerns, and the hooks reflect that.
  • useParams returns the captured path values. The values are strings, and they include the parameters from the parent routes in the matched chain. A numeric parameter must be parsed and validated.
  • useSearchParams returns a URLSearchParams object and a setter. The object has get, getAll, has, and the rest. The setter updates the query string and triggers a re-render.
  • The setter replaces the query string by default. To merge a single parameter with the existing ones, pass a function that receives the current params, modifies them, and returns them.
  • The replace option keeps the history clean. A filter change that adds a history entry for every keystroke clutters the back button. The replace: true option replaces the entry instead of adding one.
  • URL values are user-controlled and must be validated. A page parameter that is not a number, or a teamId that does not exist, must be handled gracefully. The component treats the URL as untrusted input.
  • The URL is the complete state of the view. A copied URL reproduces the same page, because both the path and the query string are in the URL. This is the reason for putting view configuration in the query string rather than in component state.
  • Effects must depend on the parameters. An effect that fetches data based on a teamId must include teamId in its dependency array. Otherwise the effect does not rerun when the parameter changes, and the component shows stale data.

Remember: React Router gives the URL two channels for state: the path and the query string. The path identifies the resource, and the query string configures the view. The useParams hook reads the path parameters, and the useSearchParams hook reads and writes the query string. Both are strings, both are user-controlled, and both must be validated before use. The path parameter changes when the resource changes, and the component re-renders with the new value. The search parameter changes when the view changes, and the setter updates the URL without navigating to a different route. The replace option keeps filter changes out of the history. The result is a URL that is the complete state of the view, shareable and bookmarkable, with the path and the query string each serving their own purpose.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!