Why Navigation Hooks Matter in Interviews
React Router v6 replaced the v5 component-centric API (<Redirect>, useHistory, useRouteMatch) with a focused set of hooks. Interviewers test navigation hooks because they expose whether a candidate understands the browser History API, controlled state vs. URL state, and the lifecycle of a navigation event inside a React app. Getting these wrong in production means broken Back buttons, unshareable filter URLs, or infinite render loops — all high-visibility bugs.
The hooks are composable building blocks. useNavigate handles imperative redirects. useParams and useLocation read the current URL. useSearchParams manages the query string as first-class state. useMatch and useBlocker cover edge cases that trip up even senior engineers. Knowing when to reach for each one — and the sharp edges around each — is what separates a strong answer from a vague one.
useNavigate — push, replace, back, and state
useNavigate returns a navigate function. The most common call is navigate('/path'), which pushes a new entry onto the history stack so the user can press Back. Passing { replace: true } replaces the current entry — correct after a login redirect so the user can't press Back into the login form.
function LoginPage() {
const navigate = useNavigate();
async function handleSubmit(e) {
e.preventDefault();
await authenticateUser(credentials);
// replace: true — login page should not be reachable via Back after success
navigate('/dashboard', { replace: true });
}
return <form onSubmit={handleSubmit}>...</form>;
}
Pass an integer to move through history: navigate(-1) goes back one step, navigate(1) goes forward. Always prefer this over window.history.back() — the native call bypasses React Router's blockers and transition listeners.
You can attach state to any navigation: navigate('/checkout', { state: { productId: 42 } }). The state is stored in the History API entry and read on the destination via useLocation().state. It is invisible in the URL and disappears on a hard refresh, so treat it as a session-only hint rather than durable data.
Critical interview trap: calling navigate() during render (outside a handler or effect) triggers an immediate re-render loop and the React warning about updating a component while rendering another. Always put navigate calls inside event handlers or useEffect.
useParams and useLocation
useParams returns an object keyed by the dynamic segments declared in your route path. For <Route path="/users/:userId/posts/:postId">, the component gets { userId, postId } — both as strings. Always parse numbers before arithmetic (Number(params.postId)) and guard against undefined for optional segments.
useLocation returns the full current location object:
| Field | Example | Notes |
|---|---|---|
pathname | /products/42 | The path without query or hash |
search | ?sort=price&page=2 | Raw query string including ? |
hash | #reviews | Fragment including # |
state | { from: '/cart' } | Attached by navigate(); lost on refresh |
key | 'abc123' | Unique per history entry |
function BreadcrumbBack() {
const { state } = useLocation();
const navigate = useNavigate();
// Graceful fallback if the user arrived via a direct URL (state is undefined)
const back = state?.returnPath ?? '/';
return <button onClick={() => navigate(back)}>← Back</button>;
}
Put location in useEffect dependency arrays to re-run logic whenever the user navigates — this is the correct pattern for analytics page-view tracking.
useSearchParams — reading and writing query strings
useSearchParams is to query strings what useState is to component state — it returns [searchParams, setSearchParams]. The searchParams value is a live URLSearchParams instance; call .get('key') for a single value or .getAll('key') for repeated keys.
function ProductList() {
const [searchParams] = useSearchParams();
const category = searchParams.get('category') ?? 'all'; // null → 'all'
const page = Number(searchParams.get('page') ?? '1');
return <p>{category} — page {page}</p>;
}
When writing, the callback form of setSearchParams is the safe default because it receives the current params and lets you mutate a copy without losing other keys:
function SortControl() {
const [searchParams, setSearchParams] = useSearchParams();
function handleSort(field) {
setSearchParams(
prev => {
const next = new URLSearchParams(prev); // clone
next.set('sort', field); // update one key
next.delete('page'); // reset pagination when sort changes
return next;
},
{ replace: true } // don't add a history entry on every sort toggle
);
}
return <button onClick={() => handleSort('price')}>Sort by price</button>;
}
Passing a plain object to setSearchParams replaces all params — a common bug when a developer only wants to change one key. The second argument { replace: true } prevents the Back button from unwinding each individual filter click, which is almost always the right UX for search/filter controls.
Choose useSearchParams over useState whenever the filter values need to survive a refresh, be shareable via URL, or be indexable by search engines. Reserve useState for purely visual, ephemeral UI state.
useMatch and useBlocker
useMatch tests the current URL against a pattern and returns a match object or null. Its main use case is conditional styling — highlighting an active nav section without relying on <NavLink>.
function SettingsLink() {
// end: false matches /settings AND any /settings/* child route
const active = useMatch({ path: '/settings', end: false });
return (
<a href="/settings" style={{ fontWeight: active ? '700' : '400' }}>
Settings
</a>
);
}
The end option mirrors how <NavLink>'s end prop works: end: true (default) requires an exact match; end: false matches any path that starts with the pattern. Use end: false for parent nav items that have child routes.
useBlocker (added in v6.4) intercepts navigations before they happen. It takes a predicate function and returns a blocker object. When the predicate returns true, the blocker enters state: 'blocked' and you must call blocker.proceed() or blocker.reset() to resolve it:
function EditForm({ isDirty }) {
const blocker = useBlocker(
({ currentLocation, nextLocation }) =>
isDirty && currentLocation.pathname !== nextLocation.pathname
);
return (
<>
<form>...</form>
{blocker.state === 'blocked' && (
<dialog open>
<p>Unsaved changes — leave anyway?</p>
<button onClick={blocker.proceed}>Leave</button>
<button onClick={blocker.reset}>Stay</button>
</dialog>
)}
</>
);
}
Never leave a blocker in 'blocked' state without resolving it — the app will be stuck unable to navigate. Note that useBlocker only intercepts React Router navigations; a user typing a new URL directly or pressing the browser's native Back button before React hydrates can still bypass it. For critical guard flows, also attach a beforeunload event listener.
Common Interview Questions at a Glance
- What does
navigate(-1)do? Moves back one history entry via React Router — prefer it overwindow.history.back()because it respects blockers and transition listeners. - How do you prevent losing query params when updating one param? Use the callback form:
setSearchParams(prev => { const n = new URLSearchParams(prev); n.set('key', val); return n; }). - When does
location.statedisappear? On a hard page refresh (F5), because the History API entry is cleared. Always provide a fallback when readingstate?.field. - What is the difference between
useMatchanduseRouteMatch?useRouteMatchwas the v5 API. In v6,useMatchreplaces it with a cleaner interface; there is nouseRouteMatchin React Router v6. - Why can't you call
navigate()during render? It triggers a state update while React is already rendering, causing an infinite re-render loop and a React invariant warning. - How do you implement an unsaved-changes guard? Use
useBlockerwith a predicate that returnstruewhen the form is dirty; render a confirmation dialog and callblocker.proceed()orblocker.reset(). - When would you use
useRoutesover JSX<Routes>? When the route tree is dynamic — generated at runtime from an API, a permissions matrix, or a CMS — so you cannot write it as static JSX. - What is
useHreffor? It resolves a path to a full href string that accounts for the router'sbasename, making it safe to use in clipboard operations, meta tags, or non-anchor elements without hardcoding path prefixes.