Client-Side Routing
SPA Navigation Without Page Reloads
LinkedIn Hook
Your React app has five pages. The user clicks a link. The entire browser refreshes, the white flash appears, all component state resets, and the loading spinner starts from scratch.
This is not how single-page applications are supposed to work.
Client-side routing means the browser never reloads. The URL changes, the correct component renders, and the rest of the app stays intact. Your navbar does not unmount. Your audio player keeps playing. Your form draft does not disappear.
React Router v6 is the standard library for this, and interviewers expect you to know it inside out. Not just
<Route>and<Link>— they want nested routes,<Outlet>, the difference between<Link>and<NavLink>, and how the URL stays in sync with the UI without ever hitting the server.
In this lesson, I break down the mental model behind client-side routing, walk through React Router v6 from the ground up —
BrowserRouter,Routes,Route,Link,NavLink, nested routes, andOutlet— and explain how the browser History API makes it all possible under the hood.
If you have ever wondered why your React app reloads when you use an
<a>tag instead of<Link>— this lesson answers that and everything else interviewers ask about routing.
Read the full lesson -> [link]
#React #JavaScript #InterviewPrep #Frontend #ReactRouter #ClientSideRouting #SPA #CodingInterview #100DaysOfCode
What You'll Learn
- What client-side routing is and why single-page applications need it
- How the browser History API (
pushState,popstate) enables URL changes without page reloads - How to set up React Router v6 with
BrowserRouter,Routes, andRoute - The difference between
<Link>and<NavLink>and when to use each - How nested routes and
<Outlet>create shared layouts without re-mounting parent components - How the URL stays in sync with the UI and why this matters for user experience and interviews
The Concept — Client-Side Routing
Analogy: The Hotel Lobby TV
Imagine a hotel lobby with a single large TV screen. A guest walks up and presses a button on the remote: "Weather." The screen changes to show the weather channel. Another guest presses "News." The screen changes again — but the TV itself never turned off. It never rebooted. The frame, the speakers, the power — all stayed on. Only the content on the screen changed.
Traditional websites work like switching between different TVs in different rooms. You click a link, the browser throws away the entire current page, sends a request to the server, and loads a completely new HTML document. Every click is walking to a new room.
A single-page application works like that lobby TV. The "frame" — your navbar, sidebar, footer, and app shell — stays mounted. When the URL changes, React swaps out only the content area. The browser never navigates away. No white flash. No full reload. No lost state.
React Router is the remote control. It watches the URL, matches it against your defined routes, and tells React which component to display in the content area. The browser's History API is the mechanism that makes the URL change without triggering a server request — history.pushState() updates the address bar, and the popstate event fires when the user clicks back or forward.
How It Works Under the Hood
Before diving into React Router code, you need to understand the browser primitive it builds on.
Traditional navigation: user clicks <a href="/about"> -> browser sends GET request to server -> server returns new HTML -> browser replaces entire page.
Client-side routing: user clicks <Link to="/about"> -> JavaScript calls history.pushState() -> browser updates the URL bar but sends NO request -> React Router sees the new URL -> it renders the matching component.
The key insight: the URL is just a piece of state. React Router reads the current URL, matches it against your route definitions, and renders the corresponding component. When the URL changes (via Link click or browser back/forward), React Router re-evaluates the match and React re-renders only the parts that changed.
Setting Up React Router v6
Code Example 1: Basic Route Setup
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
// Page components — each one represents a different "page"
function Home() {
return <h1>Home Page</h1>;
}
function About() {
return <h1>About Page</h1>;
}
function Contact() {
return <h1>Contact Page</h1>;
}
// App component — defines the route structure
function App() {
return (
// BrowserRouter wraps the entire app — provides routing context
// It uses the browser History API under the hood
<BrowserRouter>
{/* Navigation links — these do NOT cause a page reload */}
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
</nav>
{/* Routes container — only one Route matches at a time */}
<Routes>
{/* Each Route maps a URL path to a component */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</BrowserRouter>
);
}
// User clicks "About" link:
// - URL changes to /about (no page reload)
// - <Routes> evaluates all <Route> paths
// - /about matches the second route
// - <About /> renders in place of <Routes>
// - <nav> stays mounted — never unmounts or re-mounts
Key points:
BrowserRouterprovides the routing context to the entire component treeRoutesis the container that evaluates which singleRoutematches the current URLRoutemaps apathto anelement(the component to render)Linkchanges the URL without a page reload — it callshistory.pushState()internally
Code Example 2: Link vs NavLink
import { BrowserRouter, Routes, Route, Link, NavLink } from "react-router-dom";
function Navbar() {
return (
<nav>
{/* Link — basic navigation, no active state awareness */}
<Link to="/">Home</Link>
{/* NavLink — knows if it matches the current URL */}
{/* It automatically gets an "active" class when the path matches */}
<NavLink
to="/dashboard"
className={({ isActive }) =>
isActive ? "nav-link active" : "nav-link"
}
>
Dashboard
</NavLink>
{/* NavLink with inline style based on active state */}
<NavLink
to="/settings"
style={({ isActive }) => ({
fontWeight: isActive ? "bold" : "normal",
color: isActive ? "#61dafb" : "#ffffff",
})}
>
Settings
</NavLink>
{/* NavLink with the "end" prop — only active on exact match */}
{/* Without "end", NavLink to="/" would be active on EVERY page */}
{/* because every path starts with / */}
<NavLink to="/" end>
Home (exact)
</NavLink>
</nav>
);
}
// When the URL is /dashboard:
// - "Dashboard" NavLink: isActive = true -> class="nav-link active"
// - "Settings" NavLink: isActive = false -> color="#ffffff", fontWeight="normal"
// - "Home (exact)" NavLink: isActive = false (because "end" requires exact match)
Interview takeaway: Link is for basic navigation. NavLink is for navigation elements that need visual feedback on the current route (navbars, sidebars, tabs). The end prop prevents parent paths from always matching as active.
Code Example 3: Nested Routes and Outlet
import {
BrowserRouter,
Routes,
Route,
Link,
Outlet,
} from "react-router-dom";
// Layout component — the "frame" that wraps all dashboard pages
// Outlet renders whichever child route matches the current URL
function DashboardLayout() {
return (
<div>
<h1>Dashboard</h1>
<nav>
<Link to="/dashboard">Overview</Link>
<Link to="/dashboard/analytics">Analytics</Link>
<Link to="/dashboard/settings">Settings</Link>
</nav>
{/* Outlet = "render my matching child route here" */}
{/* This is where nested route components appear */}
<div className="dashboard-content">
<Outlet />
</div>
</div>
);
}
// Child page components
function Overview() {
return <p>Dashboard overview with key metrics.</p>;
}
function Analytics() {
return <p>Charts and analytics data.</p>;
}
function Settings() {
return <p>User settings and preferences.</p>;
}
function App() {
return (
<BrowserRouter>
<Routes>
{/* Parent route — renders DashboardLayout */}
{/* Child routes are nested inside */}
<Route path="/dashboard" element={<DashboardLayout />}>
{/* index route — renders when URL is exactly /dashboard */}
<Route index element={<Overview />} />
{/* /dashboard/analytics */}
<Route path="analytics" element={<Analytics />} />
{/* /dashboard/settings */}
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
</BrowserRouter>
);
}
// URL: /dashboard
// Renders: DashboardLayout -> Outlet renders <Overview />
//
// URL: /dashboard/analytics
// Renders: DashboardLayout -> Outlet renders <Analytics />
//
// URL: /dashboard/settings
// Renders: DashboardLayout -> Outlet renders <Settings />
//
// In ALL cases, DashboardLayout (header + nav) stays mounted.
// Only the Outlet content changes. No re-mount, no lost state.
Key points:
- Nested
<Route>elements inside a parent<Route>create a layout hierarchy - The parent route renders a layout component that includes
<Outlet /> <Outlet />is a placeholder that renders whichever child route matches- The
indexroute matches when the URL is exactly the parent path (no additional segment) - The parent layout never unmounts when switching between child routes
Code Example 4: 404 Not Found Route
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
function Home() {
return <h1>Home</h1>;
}
function About() {
return <h1>About</h1>;
}
// Catch-all component for unknown URLs
function NotFound() {
return (
<div>
<h1>404 — Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<Link to="/">Go back home</Link>
</div>
);
}
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
{/* Wildcard route — matches any URL that no other route matched */}
{/* Must be placed last — Routes evaluates top to bottom */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
);
}
// URL: / -> renders <Home />
// URL: /about -> renders <About />
// URL: /xyz -> no match for / or /about -> falls through to * -> renders <NotFound />
// URL: /foo/bar -> same, no match -> renders <NotFound />
Common Mistakes
Mistake 1: Using <a> tags instead of <Link> for internal navigation
// BAD: Anchor tag causes a full page reload
// The browser sends a request to the server, the entire React app unmounts,
// all component state is lost, and the app re-initializes from scratch
function Navbar() {
return (
<nav>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
);
}
// GOOD: Link component uses history.pushState() — no reload
// The React app stays mounted, state is preserved, only the route content changes
function Navbar() {
return (
<nav>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
</nav>
);
}
// Use <a> tags ONLY for external links (different domain)
// Use <Link> or <NavLink> for ALL internal navigation
Mistake 2: Forgetting the index route in nested layouts
// BAD: No index route — visiting /dashboard shows the layout with an empty Outlet
<Route path="/dashboard" element={<DashboardLayout />}>
<Route path="analytics" element={<Analytics />} />
<Route path="settings" element={<Settings />} />
</Route>
// URL: /dashboard -> DashboardLayout renders, but Outlet has nothing to show
// The user sees the sidebar and header but the content area is blank
// GOOD: index route provides a default child for the parent path
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<Overview />} />
<Route path="analytics" element={<Analytics />} />
<Route path="settings" element={<Settings />} />
</Route>
// URL: /dashboard -> DashboardLayout renders, Outlet shows <Overview />
Mistake 3: Not wrapping Routes inside BrowserRouter
// BAD: Routes used outside BrowserRouter — crashes with an error
// "useRoutes() may be used only in the context of a <Router> component"
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
</Routes>
);
}
// GOOD: BrowserRouter wraps the entire app at the top level
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</BrowserRouter>
);
}
// BrowserRouter is typically placed in index.js or main.jsx
// wrapping the entire <App /> component
Interview Questions
Q: What is client-side routing and how does it differ from server-side routing?
In server-side routing, every link click sends a request to the server, which returns a new HTML page. The browser fully reloads, and all client-side state is lost. In client-side routing, JavaScript intercepts the navigation, updates the URL using the History API (
history.pushState()), and React renders the matching component — all without a page reload. The server is never contacted for navigation. This means the app shell (navbar, sidebar) stays mounted, state is preserved, and transitions are instant.
Q: What is the difference between <Link> and <NavLink> in React Router?
Both prevent full page reloads by using
history.pushState()internally. The difference is thatNavLinkis aware of whether it matches the current URL. It provides anisActiveboolean to itsclassNameandstyleprops, allowing you to apply active styling.Linkhas no concept of active state. UseLinkfor general navigation andNavLinkfor navigation elements (navbars, tabs, sidebars) where the user needs visual feedback on which page they are on. Theendprop onNavLinkensures it only matches on exact path match, which prevents a root path like/from being active on every page.
Q: How do nested routes work in React Router v6?
A parent
<Route>wraps child<Route>elements. The parent renders a layout component that includes an<Outlet />. When the URL matches a child route, React Router renders the parent layout first, then renders the child component inside the<Outlet />. The parent layout stays mounted when switching between child routes — only the Outlet content changes. Theindexroute acts as the default child when the URL matches the parent path exactly. This pattern is used for dashboards, settings pages, and any UI where a shared layout wraps multiple sub-pages.
Q: What happens if you use a regular <a> tag instead of <Link> in a React SPA?
The browser performs a full page navigation — it sends a GET request to the server, discards the entire current page, and loads a new HTML document. In a React SPA, this means the entire React app unmounts and re-initializes from scratch. All component state, context values, and in-memory data are lost. The user sees a white flash as the page reloads.
<Link>prevents this by callinghistory.pushState()instead, which updates the URL without a server request and lets React Router handle the rendering.
Q: What is <Outlet /> and where does it go?
<Outlet />is a component provided by React Router that acts as a placeholder for child route content. It goes inside the parent route's layout component — wherever you want the child route to render. Think of it as{children}but for routes. When the URL matches a nested route, React Router renders the parent layout and places the matched child component where<Outlet />is positioned. Without<Outlet />in the parent, child routes have nowhere to render and will not appear on screen.
Quick Reference — Cheat Sheet
CLIENT-SIDE ROUTING — REACT ROUTER v6
=======================================
Core components:
BrowserRouter — Wraps the app, provides routing context (uses History API)
Routes — Container that evaluates which single Route matches
Route — Maps a URL path to a component (element prop)
Link — Navigation without page reload (replaces <a> for internal links)
NavLink — Link with active state awareness (isActive for styling)
Outlet — Placeholder in parent layout where child routes render
Route structure:
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} /> // catch-all 404
</Routes>
</BrowserRouter>
Nested routes:
<Route path="/dashboard" element={<Layout />}>
<Route index element={<Overview />} /> // /dashboard (default)
<Route path="analytics" element={<Analytics />} /> // /dashboard/analytics
<Route path="settings" element={<Settings />} /> // /dashboard/settings
</Route>
// Layout component must include <Outlet /> where children render
NavLink active styling:
<NavLink
to="/about"
className={({ isActive }) => isActive ? "active" : ""}
end // exact match only (prevents / from always being active)
>
+---------------------+------------------------------+
| Traditional (<a>) | Client-side (<Link>) |
+---------------------+------------------------------+
| Full page reload | No reload |
| Server request | No server request |
| State lost | State preserved |
| White flash | Instant transition |
| New HTML document | Same app, new component |
+---------------------+------------------------------+
Under the hood:
Link click -> history.pushState() -> URL updates -> no server request
Router listens -> matches URL to Route -> renders matched element
Back button -> popstate event -> Router re-matches -> renders previous route
Key rules:
- Always use <Link>/<NavLink> for internal navigation, <a> for external only
- Always wrap <Routes> inside <BrowserRouter>
- Always include an index route for nested layouts
- Use path="*" as the last route for 404 handling
- Use the "end" prop on NavLink to="/" to prevent it from always being active
Previous: Lesson 6.4 -- Zustand, Jotai & Modern Alternatives -> Next: Lesson 7.2 -- Dynamic Routes & Navigation ->
This is Lesson 7.1 of the React Interview Prep Course -- 10 chapters, 42 lessons.