useParams returns an object whose keys match the dynamic segments declared in the route path (prefixed with :). Use it whenever a component needs to read a URL parameter like an ID or slug.
// Route definition
// <Route path="/users/:userId/posts/:postId" element={<PostDetail />} />
function PostDetail() {
const { userId, postId } = useParams();
// Both values are strings — always parse numbers before comparing
const numericPostId = Number(postId);
return <p>User {userId}, Post {numericPostId}</p>;
}
Rule of thumb: Always treat useParams values as strings and validate/parse them before use — a missing or malformed param will be undefined, not null.
useLocation returns the current location object with five fields: pathname (the path string), search (the raw query string including ?), hash (the fragment including #), state (arbitrary data attached by navigate), and key (a unique string per history entry).
import { useLocation } from 'react-router-dom';
function DebugLocation() {
const location = useLocation();
// location.pathname → '/products/42'
// location.search → '?sort=price&order=asc'
// location.hash → '#reviews'
// location.state → { from: '/cart' } (if set by navigate)
// location.key → 'default' | 'abc123'
return <pre>{JSON.stringify(location, null, 2)}</pre>;
}
Rule of thumb: Use useLocation to react to URL changes in side effects — put location in a useEffect dependency array to re-run whenever the user navigates.
useSearchParams returns a [searchParams, setSearchParams] tuple where searchParams is a URLSearchParams instance. Call .get('key') for a single value or .getAll('key') for repeated values.
import { useSearchParams } from 'react-router-dom';
function ProductList() {
const [searchParams] = useSearchParams();
const category = searchParams.get('category'); // 'electronics' | null
const page = Number(searchParams.get('page') ?? '1'); // default to 1
const tags = searchParams.getAll('tag'); // ['sale', 'new']
return <p>Showing {category} — page {page}</p>;
}
Rule of thumb: Always provide a fallback when calling .get() — it returns null if the parameter is absent, which will cause bugs if you use it as a number or array without checking.
Call setSearchParams with a callback to get the current params and mutate a copy, or pass a plain object to replace all params. Passing the function form is safer when you only want to update a subset.
function SortControl() {
const [searchParams, setSearchParams] = useSearchParams();
function handleSort(field) {
setSearchParams(prev => {
// Clone so we don't mutate the live object
const next = new URLSearchParams(prev);
next.set('sort', field); // update one key
next.delete('page'); // reset pagination on sort change
return next;
});
}
return <button onClick={() => handleSort('price')}>Sort by price</button>;
}
Rule of thumb: Always use the callback form of setSearchParams when you need to preserve existing params — passing a plain object silently drops every key you don't include.
Pass { replace: true } as the second argument to setSearchParams to replace the current history entry instead of pushing a new one. This is important for filters and sort controls where you don't want the Back button to undo each individual filter change.
function FilterPanel() {
const [searchParams, setSearchParams] = useSearchParams();
function setFilter(key, value) {
setSearchParams(
prev => { const n = new URLSearchParams(prev); n.set(key, value); return n; },
{ replace: true } // don't pollute the history stack with every filter tick
);
}
return <input onChange={e => setFilter('q', e.target.value)} />;
}
Rule of thumb: Use replace: true for search/filter inputs that update on every keystroke; use the default push for intentional navigation actions the user might want to undo.
useMatch tests whether the current URL matches a given path pattern and returns a match object with params, pathname, and pathnameBase if it matches, or null if it does not. It is useful for styling active links or conditionally rendering UI based on route context.
import { useMatch } from 'react-router-dom';
function NavItem({ to, label }) {
// Returns non-null only when the current URL matches '/settings/*'
const match = useMatch({ path: to, end: false });
return (
<a
href={to}
style={{ fontWeight: match ? 'bold' : 'normal' }} // highlight active section
>
{label}
</a>
);
}
Rule of thumb: Use end: false to match a prefix (like /settings matching /settings/profile); use the default end: true for exact matches only.
useRoutes accepts a route config array (the same shape as the <Routes>/<Route> JSX tree) and returns the matched element or null. It is the hook-based equivalent of <Routes> and is useful when route configuration is data-driven — loaded from an API, a CMS, or a permissions table.
import { useRoutes } from 'react-router-dom';
const routes = [
{ path: '/', element: <Home /> },
{
path: '/dashboard',
element: <DashboardLayout />,
children: [
{ index: true, element: <DashboardHome /> },
{ path: 'reports', element: <Reports /> },
],
},
{ path: '*', element: <NotFound /> },
];
function App() {
// Renders whichever element matches the current URL
const element = useRoutes(routes);
return element;
}
Rule of thumb: Use useRoutes when your route tree is dynamic or generated at runtime; prefer declarative JSX <Routes> for static trees because it is easier to read at a glance.
useLinkClickHandler returns an onClick handler for a custom anchor element that performs client-side navigation the same way <Link> does — handling modifier keys (Ctrl/Cmd-click to open in new tab), target attributes, and event.preventDefault(). Use it when you need a fully custom link component that cannot extend <Link>.
import { useLinkClickHandler } from 'react-router-dom';
// A styled anchor that wraps an icon + label but still SPA-navigates
function FancyLink({ to, children, ...rest }) {
const handleClick = useLinkClickHandler(to, {
replace: false, // default push behavior
state: { from: 'fancy' },
});
return (
// Must be a real <a> so the browser handles right-click → "Open in new tab"
<a href={to} onClick={handleClick} {...rest}>
{children}
</a>
);
}
Rule of thumb: Only reach for useLinkClickHandler when you cannot use <Link> or <NavLink> as the root element — for most cases those components are sufficient.
useHref converts a to value (relative or absolute path) into a fully resolved href string that accounts for any basename configured on the router. If your app is mounted at /app, useHref('/dashboard') returns /app/dashboard, whereas a hardcoded string would break.
import { useHref } from 'react-router-dom';
function ExternalShareButton({ to }) {
// Resolves against the router's basename, giving a correct absolute path
const href = useHref(to);
const fullUrl = `${window.location.origin}${href}`;
return (
<button onClick={() => navigator.clipboard.writeText(fullUrl)}>
Copy link
</button>
);
}
Rule of thumb: Use useHref whenever you need the resolved URL as a string (for clipboard, meta tags, or non-anchor elements) rather than navigating directly — it is the basename-aware alternative to manually concatenating strings.
useBlocker accepts a condition function that receives { currentLocation, nextLocation, historyAction } and returns true to block. When blocked, it returns a blocker object with state: 'blocked' and proceed()/reset() methods to confirm or cancel.
import { useBlocker } from 'react-router-dom';
function EditForm({ isDirty }) {
const blocker = useBlocker(
// Block only when there are unsaved changes
({ currentLocation, nextLocation }) =>
isDirty && currentLocation.pathname !== nextLocation.pathname
);
return (
<>
<form>...</form>
{blocker.state === 'blocked' && (
<dialog open>
<p>You have unsaved changes. Leave anyway?</p>
<button onClick={blocker.proceed}>Leave</button> {/* confirm */}
<button onClick={blocker.reset}>Stay</button> {/* cancel */}
</dialog>
)}
</>
);
}
Rule of thumb: Always call either blocker.proceed() or blocker.reset() to resolve a blocked navigation — leaving the blocker in 'blocked' state permanently locks the app.
location.state is stored in the browser's History API entry for that specific navigation. It persists as long as the user stays in the session history — navigating back then forward re-exposes the same state. However, it is lost on a hard refresh (F5) and is not visible in the URL, so it cannot be bookmarked or shared.
// Send state when navigating to a list item
navigate(`/orders/${id}`, { state: { returnPath: '/orders', filters } });
// Read state on the destination page
function OrderDetail() {
const { state } = useLocation();
const returnPath = state?.returnPath ?? '/'; // graceful fallback for direct visits
return (
<button onClick={() => navigate(returnPath, { state: state?.filters })}>
← Back to orders
</button>
);
}
Rule of thumb: Use location.state for ephemeral UI data (breadcrumb hints, scroll position, pre-filled values); always provide a sensible fallback for when the user arrives directly via a bookmarked URL.
React Router v6 does not support the ? optional segment syntax directly. The idiomatic approach is to declare two sibling routes — one with the segment and one without — and let each render the same component. Inside the component, useParams returns undefined for the missing key when the shorter route matches.
// Route config — two routes, one component
<Routes>
<Route path="/reports/:year/:month" element={<ReportPage />} />
<Route path="/reports/:year" element={<ReportPage />} />
</Routes>
function ReportPage() {
const { year, month } = useParams();
// month is undefined when only /reports/2026 is visited
const label = month ? `${year}/${month}` : `${year} (all months)`;
return <h1>{label}</h1>;
}
Rule of thumb: Avoid trying to make a single route handle both cases with regex tricks — two explicit routes are clearer, more maintainable, and easier to test.
Use useSearchParams when the filter state should be shareable, bookmarkable, or survive a refresh — the URL is the source of truth. Use useState for purely ephemeral UI state (dropdown open/close, hover effects) that has no meaning outside the current render.
// ✅ useSearchParams — user can share/bookmark filtered results
function ProductFilter() {
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get('category') ?? 'all';
return (
<select
value={category}
onChange={e =>
setSearchParams({ category: e.target.value }, { replace: true })
}
>
<option value="all">All</option>
<option value="books">Books</option>
</select>
);
}
Rule of thumb: If a user refreshing the page or copying the URL should see the same results, that state belongs in the URL (useSearchParams); if it is purely visual/transient, useState is sufficient.
The end option (default true) controls whether the pattern must match the entire pathname or just a prefix. With end: true, /settings only matches the exact path /settings. With end: false, it matches /settings, /settings/profile, /settings/billing, and so on — useful for highlighting a parent nav item when any child route is active.
function SettingsNav() {
// end: false — active whenever any /settings/* route is rendered
const settingsMatch = useMatch({ path: '/settings', end: false });
// end: true (default) — active only on the exact /settings page
const exactMatch = useMatch('/settings');
return (
<nav>
<a style={{ fontWeight: settingsMatch ? 'bold' : 'normal' }}
href="/settings">
Settings {settingsMatch && '(active section)'}
</a>
</nav>
);
}
Rule of thumb: Set end: false for top-level nav items that have child routes; use the default end: true (or just pass the path string) for leaf routes where exact matching matters.
More Routing interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.