Client-side routing means the browser never performs a full page reload when navigating between views. Instead, JavaScript intercepts navigation events, updates the URL via the History API, and re-renders the appropriate component tree — giving the feel of a multi-page app without the network round-trip.
// Without client-side routing: browser fetches /about from the server
// With client-side routing: JS swaps components and pushes to history
import { Link } from 'react-router-dom';
function Nav() {
return (
// Clicking this never triggers a server request
<Link to="/about">About</Link>
);
}
Rule of thumb: Use client-side routing in any React SPA so users get instant navigation and the app shell (header, sidebar) stays mounted across route changes.
Install the package, then wrap your app in BrowserRouter (or use createBrowserRouter for the data router API). Every router hook and component must live inside that provider.
// 1. npm install react-router-dom
// 2. main.jsx — wrap the root
import { BrowserRouter } from 'react-router-dom';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')).render(
<BrowserRouter> {/* provides routing context to the whole tree */}
<App />
</BrowserRouter>
);
Rule of thumb: Always place BrowserRouter (or its equivalent) at the very root of your component tree, not inside a page component — otherwise hooks like useNavigate will throw a context error.
createBrowserRouter is the v6.4+ data router API. It co-locates routes with loaders and actions (server-style data fetching/mutation), and it is the recommended approach for new apps. BrowserRouter is the legacy component wrapper that does not support loaders/actions.
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 with <Outlet />
children: [
{ index: true, element: <Home /> },
{ path: 'about', element: <About /> },
],
},
]);
// main.jsx
<RouterProvider router={router} />
Rule of thumb: Reach for createBrowserRouter in greenfield projects; use BrowserRouter only when you need to stay on the simpler JSX-only API or are migrating incrementally.
Routes is a container that picks the single best-matching Route from its children. Route declares a path and the element to render when the URL matches. In v6, Routes replaces the v5 Switch and always does exclusive (first-match) selection.
import { Routes, Route } from 'react-router-dom';
import Home from './Home';
import About from './About';
import NotFound from './NotFound';
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} /> {/* catch-all */}
</Routes>
);
}
Rule of thumb: Always keep your top-level Routes block in one place (usually App.jsx) so route precedence is obvious at a glance.
In v6 all routes match exactly by default — the exact prop was removed. A route with path="/" will not match /about; you no longer need to add exact everywhere. The router also ranks routes by specificity, so more-specific paths win over less-specific ones.
<Routes>
{/* v5 needed exact on "/", v6 does not */}
<Route path="/" element={<Home />} /> {/* only "/" */}
<Route path="/users" element={<Users />} /> {/* only "/users" */}
<Route path="/users/:id" element={<User />} /> {/* wins over "/users" for "/users/42" */}
</Routes>
Rule of thumb: Remove all exact props when migrating from v5 to v6 — they are a no-op and will generate a warning.
Nest Route elements inside a parent Route to model layout nesting. The parent renders its element and places an Outlet component where child routes should appear. The child path is relative to the parent — no leading slash needed.
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
{/* relative paths — resolved as /dashboard/overview */}
<Route path="overview" element={<Overview />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
// DashboardLayout.jsx
import { Outlet } from 'react-router-dom';
function DashboardLayout() {
return (
<div>
<Sidebar />
<main>
<Outlet /> {/* child route renders here */}
</main>
</div>
);
}
Rule of thumb: Use nested routes whenever multiple pages share a persistent layout (nav, sidebar, header) — the layout component stays mounted while only the Outlet content swaps.
Outlet is a placeholder rendered by a parent route's element; it marks the spot where the matched child route's component should appear. Without Outlet, child routes match in the URL but their elements are never rendered to the screen.
import { Outlet, NavLink } from 'react-router-dom';
function AdminLayout() {
return (
<div className="admin">
<nav>
<NavLink to="users">Users</NavLink>
<NavLink to="reports">Reports</NavLink>
</nav>
{/* matched child route appears here */}
<Outlet />
</div>
);
}
Rule of thumb: Every route that has children in the route tree must render <Outlet /> somewhere in its element, or those children will silently not appear.
An index route is a child route with index={true} instead of a path. It renders inside the parent's Outlet when the URL exactly matches the parent path — serving as the "default child" view.
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
{/* renders at /dashboard when no child segment follows */}
<Route index element={<DashboardHome />} />
<Route path="users" element={<Users />} />
<Route path="reports" element={<Reports />} />
</Route>
</Routes>
Rule of thumb: Add an index route to every layout route so the Outlet is never empty — without it, visiting the parent path exactly renders a blank content area.
Link renders an <a> element but intercepts the click, calls history.pushState, and re-renders the app without a page reload. A plain <a href="..."> would trigger a full browser navigation, destroying React state.
import { Link } from 'react-router-dom';
function Nav() {
return (
<nav>
<Link to="/">Home</Link> {/* absolute path */}
<Link to="settings">Settings</Link> {/* relative to current route */}
<Link to="/users/42">User 42</Link>
</nav>
);
}
Rule of thumb: Always use Link (or NavLink) for in-app navigation; reserve plain <a> tags only for external URLs or files that require a real browser navigation.
Add a <Route path="*"> as the last child of your Routes. The wildcard * matches any URL not claimed by earlier routes, making it the conventional 404 handler.
import NotFound from './NotFound';
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users/:id" element={<UserProfile />} />
{/* catches everything else */}
<Route path="*" element={<NotFound />} />
</Routes>
);
}
Rule of thumb: Always include a path="*" route — without it, unmatched URLs render nothing, which silently looks broken to users.
BrowserRouter uses the HTML5 History API (pushState) and produces clean URLs like /about. It requires the server to serve index.html for every path. HashRouter encodes the route in the URL hash (/#/about) — the hash is never sent to the server, so it works on any static host without server config.
// History mode — clean URLs, needs server catch-all
import { BrowserRouter } from 'react-router-dom';
<BrowserRouter><App /></BrowserRouter>
// Hash mode — works on GitHub Pages / plain file servers
import { HashRouter } from 'react-router-dom';
<HashRouter><App /></HashRouter>
// Server catch-all example (Express)
// app.get('*', (req, res) => res.sendFile('index.html'));
Rule of thumb: Prefer BrowserRouter with a server catch-all for production apps; use HashRouter only when you have no control over server configuration (e.g., GitHub Pages, local file:// serving).
By default browsers try to restore scroll position, but client-side navigation breaks this because the DOM updates asynchronously. React Router v6 ships a ScrollRestoration component (data router only) that saves and restores scroll position per URL entry. For BrowserRouter apps, a common manual solution is a ScrollToTop component that calls window.scrollTo(0, 0) on route change.
// Data router approach (createBrowserRouter)
import { ScrollRestoration } from 'react-router-dom';
function Root() {
return (
<>
<Nav />
<Outlet />
<ScrollRestoration /> {/* place once in the root layout */}
</>
);
}
// Manual approach for BrowserRouter
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => { window.scrollTo(0, 0); }, [pathname]);
return null;
}
Rule of thumb: Always handle scroll restoration explicitly — without it, users navigating back to a long page will be stranded at the bottom.
Child route path values inside a <Route> parent are relative — do not add a leading slash. A leading slash makes the path absolute, which breaks nesting because it is resolved from the root instead of relative to the parent.
// WRONG — leading slash makes children absolute paths
<Route path="/settings" element={<SettingsLayout />}>
<Route path="/settings/profile" element={<Profile />} /> {/* breaks */}
<Route path="/settings/security" element={<Security />} /> {/* breaks */}
</Route>
// CORRECT — relative paths, no leading slash
<Route path="/settings" element={<SettingsLayout />}>
<Route path="profile" element={<Profile />} /> {/* /settings/profile */}
<Route path="security" element={<Security />} /> {/* /settings/security */}
</Route>
Rule of thumb: Never prefix child route paths with / — keep them relative so React Router can compose parent + child segments correctly.
useLocation returns the current location object with pathname, search, hash, state, and key. It is useful for reading query params, animating on route change, or passing state between routes without putting it in the URL.
import { useLocation } from 'react-router-dom';
function Analytics() {
const location = useLocation();
useEffect(() => {
// fire a page-view event on every navigation
trackPageView(location.pathname);
}, [location.pathname]);
return null;
}
// Passing state via Link and reading it with useLocation
<Link to="/confirm" state={{ orderId: 42 }}>Confirm</Link>
function ConfirmPage() {
const { state } = useLocation();
return <p>Order {state?.orderId} confirmed</p>;
}
Rule of thumb: Use useLocation whenever you need to react to URL changes without controlling the route — analytics, animations, and reading navigation state are the classic cases.
Declare a dynamic segment in the route path with a colon prefix (:paramName). Inside the matched component, read it with the useParams hook, which returns an object keyed by every named segment in the route.
// Route declaration
<Route path="/users/:userId/posts/:postId" element={<Post />} />
// Component
import { useParams } from 'react-router-dom';
function Post() {
const { userId, postId } = useParams();
// userId and postId are always strings — convert if needed
return <p>User {userId}, Post {postId}</p>;
}
Rule of thumb: Always coerce URL params to the correct type (Number(userId)) before using them in logic — useParams always returns strings regardless of what you stored.
The to prop accepts a string (path, with optional query/hash), a partial location object, or a relative string. Relative paths are resolved against the current route's URL segment, making them useful inside nested route trees.
import { Link } from 'react-router-dom';
function Examples() {
return (
<>
{/* absolute string */}
<Link to="/users">Users</Link>
{/* string with query + hash */}
<Link to="/search?q=react#results">Search</Link>
{/* location object for programmatic control */}
<Link to={{ pathname: '/users', search: '?page=2' }}>Page 2</Link>
{/* relative — goes up one segment then to "edit" */}
<Link to="../edit">Edit</Link>
</>
);
}
Rule of thumb: Prefer string to values for simplicity; switch to the object form only when you need to set search, hash, or state independently.
Any React Router hook (useNavigate, useLocation, useParams, etc.) throws "useNavigate() may be used only in the context of a Router component" (or similar) when called outside the BrowserRouter / RouterProvider tree. The fix is always to ensure the Router wraps the component tree at or above the point where the hook is called.
// WRONG — hook runs outside the Router tree
function App() {
const navigate = useNavigate(); // throws!
return <BrowserRouter>...</BrowserRouter>;
}
// CORRECT — Router wraps everything that uses routing hooks
function Root() {
return (
<BrowserRouter>
<App /> {/* App and all descendants can now use hooks */}
</BrowserRouter>
);
}
function App() {
const navigate = useNavigate(); // works fine here
return <Routes>...</Routes>;
}
Rule of thumb: If you see a Router context error, trace the component tree upward — the component calling the hook is not a descendant of any BrowserRouter or RouterProvider.
More Routing interview questions
More ways to practice
The self-quiz is live. Get notified when mock interviews and new question packs drop.