Skip to content

React · Routing

React Router v6 Navigation Hooks — Complete Interview Guide

7 min read Updated 2026-06-24 Share:

Practice Navigation Hooks interview questions

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:

FieldExampleNotes
pathname/products/42The path without query or hash
search?sort=price&page=2Raw query string including ?
hash#reviewsFragment 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 over window.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.state disappear? On a hard page refresh (F5), because the History API entry is cleared. Always provide a fallback when reading state?.field.
  • What is the difference between useMatch and useRouteMatch? useRouteMatch was the v5 API. In v6, useMatch replaces it with a cleaner interface; there is no useRouteMatch in 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 useBlocker with a predicate that returns true when the form is dirty; render a confirmation dialog and call blocker.proceed() or blocker.reset().
  • When would you use useRoutes over 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 useHref for? It resolves a path to a full href string that accounts for the router's basename, making it safe to use in clipboard operations, meta tags, or non-anchor elements without hardcoding path prefixes.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel