useParams

A dynamic segment is a hole in a pattern: :sku in /products/:sku matches one URL segment and remembers it under that name. useParams() returns every param matched by the current route and its ancestors, so a component nested three routes deep sees the parent's :orgId alongside its own :taskId without props.

What useParams returns for three different patternsHTMLLive
<style>body{font:15px/1.7 system-ui;margin:12px}
code{background:#eceff1;padding:1px 5px;border-radius:4px}</style>
<div id="root"></div>
<script type="importmap">{"imports":{"react":"https://esm.sh/react@19.3.0",
"react-dom/client":"https://esm.sh/react-dom@19.3.0/client?external=react",
"react-router":"https://esm.sh/react-router@8.4.0?external=react"}}</script>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
<script type="text/babel" data-type="module">
import React from 'react';
import { createRoot } from 'react-dom/client';
import { MemoryRouter, Routes, Route, useParams, useLocation } from 'react-router';
function Show() {                                  // one component, three URLs
  return <p><code>{useLocation().pathname}</code> &rarr;{' '}
    <code>{JSON.stringify(useParams())}</code></p>;
}
function At({ url }) {
  return <MemoryRouter initialEntries={[url]}>
    <Routes>
      <Route path="/docs/*" element={<Show />} />
      <Route path="/:id/:page?" element={<Show />} />
    </Routes>
  </MemoryRouter>;
}
createRoot(document.getElementById('root')).render(<>
  <At url="/netflix" />
  <At url="/yahoo/invoices" />
  <At url="/docs/api/hooks" />
</>);
</script>
Browser output of Listing 2.141
Browser output of 141

Three things to read off that output. Every value is a string — /products/42 gives you "42", so call Number() before arithmetic. An optional segment that did not match is absent from the object rather than undefined. And a splat route stores the unmatched remainder under the key *, slashes included, which is how a docs viewer takes over a whole subtree.

Going the other way, generatePath('/user/:id/:entity', { id: 1, entity: 'posts' }) returns /user/1/posts; prefer it to a template literal, because it encodes the values and fails loudly on a missing one. Validate params in the component, not in the pattern — a bad :sku reaches your code, and you decide whether that means an empty state, a <Navigate>, or a thrown error for the nearest boundary.