Skip to content

Navigation Hooks Interview Questions & Answers

20 questions Updated 2026-06-24 Share:

React Router v6 navigation hooks interview questions — useNavigate, useParams, useLocation, useSearchParams, useMatch, useBlocker, and programmatic navigation patterns.

Read the in-depth guideReact Router v6 Navigation Hooks — Complete Interview Guide(opens in new tab)
20 of 20

useNavigate returns a navigate function that lets you redirect the user programmatically — typically inside event handlers, effects, or after async operations. Calling navigate('/path') performs a push (adds a history entry); passing { replace: true } performs a replace (overwrites the current entry).

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

function LoginForm() {
  const navigate = useNavigate();

  async function handleSubmit(e) {
    e.preventDefault();
    await login(credentials);
    // Push a new entry so the user can press Back to return to login
    navigate('/dashboard');
    // Or replace so pressing Back does NOT return to this form:
    // navigate('/dashboard', { replace: true });
  }

  return <form onSubmit={handleSubmit}>...</form>;
}

Rule of thumb: Use replace: true after authentication or any flow where letting the user navigate back to the previous page would be confusing or insecure.

Without options, navigate('/path') is a push: it appends a new entry to the history stack, so pressing the browser Back button returns to the previous URL. With { replace: true } it is a replace: the current history entry is overwritten and the previous URL is no longer reachable via Back.

function CheckoutSuccess() {
  const navigate = useNavigate();

  useEffect(() => {
    // Replace so users can't press Back and accidentally re-submit the order
    navigate('/order-confirmation', { replace: true });
  }, [navigate]);

  return null;
}

Rule of thumb: Prefer replace for redirects that follow a destructive or one-way action (form submit, logout, payment confirmation); prefer the default push for normal link-like navigation.

Pass the integer -1 to navigate to go back one entry, -2 for two entries, or +1 to go forward. This mirrors the window.history.go(delta) API.

function BackButton() {
  const navigate = useNavigate();

  return (
    // navigate(-1) pops the history stack by one entry
    <button onClick={() => navigate(-1)}>← Back</button>
  );
}

Rule of thumb: Use navigate(-1) instead of window.history.back() so React Router stays in control of the transition; window.history.back() bypasses React Router's transition listeners and blockers.

Pass a state key in the options object. The state is stored in the history entry and is accessible on the destination via useLocation().state. It is not reflected in the URL so it disappears on a hard refresh.

// Source page
function ProductCard({ product }) {
  const navigate = useNavigate();
  return (
    <button onClick={() => navigate('/checkout', { state: { productId: product.id } })}>
      Buy now
    </button>
  );
}

// Destination page
import { useLocation } from 'react-router-dom';

function CheckoutPage() {
  const { state } = useLocation();
  // state.productId is available here; undefined after a hard refresh
  return <p>Checking out product {state?.productId}</p>;
}

Rule of thumb: Use location state for transient UI hints (e.g., "came from search results") — never for critical data that must survive a refresh; persist that in a store or URL param instead.

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.

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.

Calling navigate() during render triggers a navigation while React is still rendering, causing an immediate re-render loop and the React warning "Cannot update a component while rendering a different component." Navigation must always happen in event handlers or inside a useEffect.

// ❌ Wrong — navigate called during render causes infinite re-render
function RedirectIfLoggedOut({ user }) {
  const navigate = useNavigate();
  if (!user) navigate('/login'); // fires on every render pass
  return <Dashboard />;
}

// ✅ Correct — navigate called inside useEffect, runs after render
function RedirectIfLoggedOut({ user }) {
  const navigate = useNavigate();
  useEffect(() => {
    if (!user) navigate('/login', { replace: true });
  }, [user, navigate]);
  return user ? <Dashboard /> : null;
}

Rule of thumb: If you find yourself writing navigate() outside of an event handler or useEffect, stop — you are almost certainly in a render path and will cause an infinite loop.

Both navigate back one history entry, but navigate(-1) goes through React Router's transition system while window.history.back() is a raw browser API call. This means React Router's blockers, scroll restoration, and transition listeners fire for navigate(-1) but are bypassed by window.history.back().

function BackButton() {
  const navigate = useNavigate();

  return (
    <>
      {/* ✅ Preferred — triggers React Router's blockers and listeners */}
      <button onClick={() => navigate(-1)}>Back (React Router)</button>

      {/* ❌ Avoid — skips useBlocker, no transition event, hard to test */}
      <button onClick={() => window.history.back()}>Back (native)</button>
    </>
  );
}

Rule of thumb: Always use navigate(-1) in React Router apps so any registered blockers (unsaved-form guards) and navigation listeners fire correctly.

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 ways to practice

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

or
Join our WhatsApp Channel