React Router v6 is the standard routing solution for React SPAs, and it appears in almost every mid-to-senior frontend interview. This guide covers everything you need to know to answer routing questions confidently — from the mental model behind client-side routing through the specific API changes that tripped up developers migrating from v5.
Why Client-Side Routing Exists
A traditional multi-page app works by making a full HTTP request for every URL. The browser discards the current DOM, fetches a new HTML document from the server, and re-paints the page. That model is simple but has a hard cost: every navigation throws away in-memory state, re-downloads shared assets, and flashes a blank screen while the network round-trip completes.
React applications are built around a persistent component tree that manages state in memory. Client-side routing preserves that tree across navigations. When a user clicks a link, JavaScript intercepts the click, calls the browser's History API (pushState or replaceState) to update the URL bar, and then re-renders only the parts of the component tree that need to change. The network is never involved.
The result is navigation that feels instant because it is instant — no network latency, no full repaint, no lost scroll position in the parts of the page that didn't change. The tradeoff is that the server must be configured to serve index.html for every URL, since there is no server-side route handler for paths like /users/42.
Installing and Setting Up React Router v6
npm install react-router-dom
There are two setup styles in v6. The simpler one wraps your app in BrowserRouter, a React context provider that makes routing hooks available to every descendant:
// main.jsx
import { BrowserRouter } from 'react-router-dom';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')).render(
<BrowserRouter> {/* routing context for the entire tree */}
<App />
</BrowserRouter>
);
The v6.4+ approach uses createBrowserRouter and RouterProvider, which unlocks the data router features (loaders, actions, fetchers). It is the recommended path for new projects:
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Root from './Root';
import Home from './Home';
import About from './About';
const router = createBrowserRouter([
{
path: '/',
element: <Root />, // layout shell with <Outlet />
children: [
{ index: true, element: <Home /> }, // default child at "/"
{ path: 'about', element: <About /> }, // "/about"
],
},
]);
createRoot(document.getElementById('root')).render(
<RouterProvider router={router} />
);
For interview purposes, know both forms. Most existing codebases still use BrowserRouter; most new projects should prefer createBrowserRouter.
Core Concept: Routes and Route
Routes scans its Route children and renders the one whose path best matches the current URL. In v6, every route matches exactly by default — the exact prop from v5 was removed entirely. Route ranking is based on specificity: more-specific paths win over wildcards, and static segments win over dynamic ones.
import { Routes, Route } from 'react-router-dom';
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users" element={<UserList />} />
<Route path="/users/:id" element={<UserDetail />} /> {/* beats /users for /users/42 */}
<Route path="*" element={<NotFound />} /> {/* catch-all 404 */}
</Routes>
);
}
Dynamic segments (:id) are accessible inside the rendered component via the useParams hook:
import { useParams } from 'react-router-dom';
function UserDetail() {
const { id } = useParams(); // always a string
return <p>User ID: {id}</p>;
}
Navigation: Link and NavLink
Link renders an <a> that intercepts clicks and performs a client-side navigation. Never use a plain <a href> for in-app navigation — it bypasses the router and triggers a full page reload.
NavLink is a Link that knows whether its destination is currently active. In v6, the activeClassName and activeStyle props from v5 were removed. Instead, pass a callback to className or style that receives an { isActive } object:
import { Link, NavLink } from 'react-router-dom';
function Navigation() {
return (
<nav>
{/* basic link — no active styling */}
<Link to="/">Home</Link>
{/* NavLink with active class callback */}
<NavLink
to="/dashboard"
className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
>
Dashboard
</NavLink>
{/* NavLink with active style callback */}
<NavLink
to="/settings"
style={({ isActive }) => ({ fontWeight: isActive ? 700 : 400 })}
>
Settings
</NavLink>
</nav>
);
}
Use NavLink in menus and sidebars; use plain Link for inline text or buttons that navigate.
Nested Routes and Outlet
Nested routes let you co-locate a persistent layout with its child views. Declare child routes inside a parent Route element — their path values are relative (no leading slash). The parent component renders an Outlet to mark where the active child should appear:
// Route tree
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} /> {/* /dashboard */}
<Route path="analytics" element={<Analytics />} /> {/* /dashboard/analytics */}
<Route path="settings" element={<Settings />} /> {/* /dashboard/settings */}
</Route>
</Routes>
// DashboardLayout.jsx
import { Outlet, NavLink } from 'react-router-dom';
function DashboardLayout() {
return (
<div className="dashboard">
<aside>
<NavLink to="analytics">Analytics</NavLink>
<NavLink to="settings">Settings</NavLink>
</aside>
<main>
<Outlet /> {/* child route renders here; layout stays mounted */}
</main>
</div>
);
}
The critical rule: never add a leading slash to child route paths. A leading slash makes the path absolute, so /settings inside a /dashboard parent would match the root-level /settings instead of /dashboard/settings.
Index Routes
An index route is the default child of a layout route. It renders in the parent's Outlet when the URL matches the parent path exactly, with nothing following. Declare it with the index boolean prop instead of a path:
<Route path="/profile" element={<ProfileLayout />}>
<Route index element={<ProfileOverview />} /> {/* /profile */}
<Route path="posts" element={<UserPosts />} /> {/* /profile/posts */}
<Route path="likes" element={<UserLikes />} /> {/* /profile/likes */}
</Route>
Without an index route, visiting /profile exactly renders the layout shell with an empty Outlet — a blank content area with no error message, which is hard to debug.
Programmatic Navigation: useNavigate
The useNavigate hook returns a navigate function for use in event handlers and effects. Pass a path string to push, or add { replace: true } to replace the current history entry:
import { useNavigate } from 'react-router-dom';
function LoginPage() {
const navigate = useNavigate();
async function handleSubmit(e) {
e.preventDefault();
await login(credentials);
// replace so pressing Back doesn't return to the login form
navigate('/dashboard', { replace: true });
}
return <form onSubmit={handleSubmit}>...</form>;
}
navigate(-1) goes back one step in history — equivalent to clicking the browser's Back button. Use replace: true after login, logout, form submissions, and payment confirmations — anywhere the previous step should not appear in the history stack.
Catch-All Routes (404 Pages)
A route with path="*" matches any URL not claimed by earlier routes. Place it last in your Routes block:
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} /> {/* always last */}
</Routes>
Without a catch-all, unmatched URLs render nothing — no error, just an empty page. Always include it.
Hash Mode vs History Mode
BrowserRouter uses the HTML5 History API, producing clean URLs like /about. It requires the web server to redirect all paths to index.html (Apache: FallbackResource /index.html; Nginx: try_files $uri /index.html).
HashRouter stores the route in the URL hash: /#/about. The hash fragment is never sent to the server, so it works on any static file host without configuration — useful for GitHub Pages or environments where you cannot control server routing. The cost is uglier URLs and no server-side rendering support.
Common Interview Questions at a Glance
- What replaced the
exactprop in v6? It was removed — all routes match exactly by default; no prop needed. - What replaced
Switchin v6? TheRoutescomponent, which also does automatic route ranking by specificity. - How do you style the active NavLink in v6? Pass a callback to
classNameorstylethat receives{ isActive }—activeClassNamewas removed. - What does Outlet do? It renders the matched child route's element inside a parent layout component.
- What is an index route? A child route with
indexinstead ofpaththat renders when the parent URL matches exactly. - How do you navigate programmatically? Use the
useNavigatehook; callnavigate(path)to push ornavigate(path, { replace: true })to replace. - How do you create a 404 page? Add
<Route path="*" element={<NotFound />} />as the last route in aRoutesblock. - What is the difference between BrowserRouter and HashRouter? BrowserRouter uses History API with clean URLs but requires server config; HashRouter encodes routes in the hash and needs no server config.