🧭 Client-side Routing Concepts
A single-page app never really "leaves" the page — yet the URL changes, the back button works, and different screens appear. That illusion is client-side routing. In this lesson you'll learn what React Router actually does under the hood and meet the handful of components that turn one HTML file into a multi-screen application.
Week 5 · Day 2 (Tuesday: React Router) · Lecture 1
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain the difference between traditional server-side routing and client-side routing
- Describe how React Router matches a browser URL to a component to render
- Install React Router and wrap an app in
BrowserRouter - Declare routes with
<Routes>and<Route element>, including a catch-all 404 - Navigate with
<Link>and highlight the current page with<NavLink> - Read dynamic URL segments with
useParamsand navigate in code withuseNavigate
Estimated Time: 55 minutes
Practice: Wire up a three-page mini-site (Home / About / Contact) plus a dynamic user profile route.
In This Lesson
What is client-side routing?
Imagine walking through a museum where, instead of you moving between rooms, the room transforms around you as you pick different exhibits. The sign above the door changes, you can retrace your steps, but you never actually walked anywhere. That is client-side routing: the URL in the address bar changes and the screen updates, but the browser never fetches a brand-new HTML document.
A React app ships as essentially one HTML file with a bundle of JavaScript. That's why we call it a single-page application (SPA). Without a router, that single page can only ever show one thing. React Router is the library that lets one page pretend to be many: it watches the URL, decides which component belongs to it, and swaps the visible content — all without a network round-trip or a white flash of reload.
Everything in this lesson orbits that one idea: a URL comes in, React Router picks the component, React renders it. Once that clicks, the rest is just syntax.
Server-side vs client-side routing
Before SPAs, every link took you back to the server. Understanding what changed makes React Router far less mysterious.
| Server-side routing | Client-side routing | |
|---|---|---|
| Who decides the page? | The server, per request | The router, in the browser |
| Network request per navigation? | Yes — a full HTML document | No — data only, if any |
| Page reload? | Full reload every time | None — content swaps in place |
| Feel | Classic website | App-like, instant |
| First load | Fast (small page) | Heavier (ship the app once) |
💡 It's not either/or
Modern frameworks blend both — the server renders the first page for speed and SEO, then the client router takes over for snappy navigation. But to understand the blend, you first need to understand plain client-side routing, which is exactly what this week covers.
How React Router matches URLs
React Router leans on the browser's built-in History API (pushState, popState). When you click a <Link>, the router calls history.pushState to change the address bar without asking the server for anything. It then compares the new path against your list of routes, finds the best match, and renders that route's element.
The magic is that step "match" — it happens in memory, in milliseconds, with no server involved. When the user presses the browser back button, the History API fires a popState event, the router sees the URL rewind, and it re-matches. That's why back/forward "just work" in a well-built SPA.
Installing & wrapping your app
React Router ships as a separate package. For web apps you install react-router-dom:
npm install react-router-dom
Then you wrap your entire component tree in a router provider. BrowserRouter is the one to reach for — it uses real, clean URLs (/about, not /#/about). Wrap it once, at the very top, usually in main.jsx:
// main.jsx — the router lives above everything else
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
{/* BrowserRouter provides routing context to every component below */}
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
⚠️ Only one router, at the top
Wrap your app in BrowserRouter once. Nesting a second router inside is a classic beginner bug that produces confusing "cannot read properties of undefined" errors, because hooks like useNavigate read from the nearest router context.
The core building blocks
With the provider in place, four pieces do almost all the work. Meet them once and you can build most apps.
1. <Routes> and <Route>
<Routes> is the switchboard: it looks at the current URL and renders the one best-matching <Route>. Each <Route> pairs a path with the component to show via the element prop.
import { Routes, Route } from 'react-router-dom';
function Home() { return <h1>Welcome Home!</h1>; }
function About() { return <h1>About Us</h1>; }
function Contact() { return <h1>Contact Us</h1>; }
function NotFound() { return <h1>404 — Page Not Found</h1>; }
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
{/* "*" is the catch-all: it matches anything not matched above */}
<Route path="*" element={<NotFound />} />
</Routes>
);
}
📖 Modern v6+ syntax
If you've seen older tutorials using <Switch> and <Route component={Home}>, that's React Router v5. Version 6+ replaced it with <Routes> and the element={<Home />} prop, which is what you'll use everywhere in this course. The new matcher is also smarter: it ranks routes and picks the best one, so order no longer matters the way it used to.
2. <Link> — navigate without reloading
This is the star of the show. A plain <a href> triggers a full page reload and throws away your app's state. <Link> renders a real anchor for accessibility but intercepts the click and hands it to the router instead.
import { Link } from 'react-router-dom';
function Navigation() {
return (
<nav>
{/* ✅ Client-side navigation — no reload */}
<Link to="/">Home</Link>
<Link to="/about">About</Link>
<Link to="/contact">Contact</Link>
</nav>
);
}
function BadNavigation() {
return (
<nav>
{/* ❌ These cause full page reloads and lose all state! */}
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
);
}
3. <NavLink> — a Link that knows it's active
<NavLink> is <Link> with a superpower: it tells you whether its destination is the page you're currently on, so you can highlight it. The className (or style) prop can be a function that receives { isActive }.
import { NavLink } from 'react-router-dom';
function Navigation() {
return (
<nav>
{/* "end" makes "/" match only the exact home path */}
<NavLink to="/" end
className={({ isActive }) => (isActive ? 'active' : '')}>
Home
</NavLink>
<NavLink to="/about"
style={({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
textDecoration: isActive ? 'underline' : 'none',
})}>
About
</NavLink>
</nav>
);
}
💡 Rule of thumb: Use<NavLink>for your main navigation bar (where highlighting the current page helps the user), and plain<Link>for one-off links inside content.
Dynamic routes & params
Hard-coding a route for every user or product is impossible — you don't know their IDs ahead of time. A dynamic segment, written with a colon, acts as a placeholder that matches any value and captures it.
// The ":userId" part is a placeholder — it matches /users/1, /users/ada, etc.
<Route path="/users/:userId" element={<UserProfile />} />
<Route path="/posts/:postId/comments/:commentId" element={<Comment />} />
Inside the matched component, the useParams hook hands you those captured values as a plain object:
import { useParams } from 'react-router-dom';
function UserProfile() {
// The key name matches the ":userId" in the route path
const { userId } = useParams();
return <h1>User Profile: {userId}</h1>;
}
function Comment() {
const { postId, commentId } = useParams();
return (
<div>
<h2>Post: {postId}</h2>
<h3>Comment: {commentId}</h3>
</div>
);
}
Output
Visiting /users/ada → "User Profile: ada"
Visiting /posts/7/comments/3 → "Post: 7" / "Comment: 3"
💡 Params vs query strings
Use a param (/products/42) to identify which resource — it's part of the route's identity. Use a query string (/products?sort=price, read with useSearchParams) for optional modifiers like sorting, filtering, or pagination. You'll go deeper on both in the dedicated route-params lesson later this week.
Practice & Quiz
🏋️ Exercise 1: A three-page mini-site
Goal: Build a tiny app with a shared nav bar and three pages. The nav should highlight whichever page is active, and any unknown URL should show a 404.
// Fill in the blanks so /, /about, and /contact each render,
// the current link is highlighted, and bad URLs show NotFound.
function App() {
return (
<div>
<nav>
{/* TODO: NavLink to "/", "/about", "/contact" */}
</nav>
{/* TODO: Routes with a Route for each page + a catch-all */}
</div>
);
}
💡 Hint
Use <NavLink> with an end prop on the "/" link so it isn't marked active on every page. Give the catch-all route path="*". The className callback receives { isActive }.
✅ Solution
import { Routes, Route, NavLink } from 'react-router-dom';
const linkClass = ({ isActive }) => (isActive ? 'active' : '');
function App() {
return (
<div>
<nav>
<NavLink to="/" end className={linkClass}>Home</NavLink>
<NavLink to="/about" className={linkClass}>About</NavLink>
<NavLink to="/contact" className={linkClass}>Contact</NavLink>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
);
}
🏋️ Exercise 2: A dynamic profile route
Goal: Add a route /users/:userId that displays the id from the URL, and a button that jumps to /users/42 in code.
✅ Solution
import { Route, useParams, useNavigate } from 'react-router-dom';
// <Route path="/users/:userId" element={<UserProfile />} />
function UserProfile() {
const { userId } = useParams();
return <h1>Viewing user #{userId}</h1>;
}
function GoToUser() {
const navigate = useNavigate();
return (
<button onClick={() => navigate('/users/42')}>
Open user 42
</button>
);
}
🎯 Quick Quiz
Question 1: What is the main advantage of client-side routing over server-side routing?
Question 2: Which component should you use for navigation to avoid a full page reload?
Question 3: Given <Route path="/users/:userId" ... />, how do you read the id inside the component?
Best Practices & Pitfalls
✅ Do
- Wrap your app in
BrowserRouterexactly once, at the top of the tree - Use
<Link>/<NavLink>for user clicks anduseNavigatefor logic-driven redirects - Always include a catch-all
<Route path="*">so unknown URLs show a friendly 404 - Give descriptive, hierarchical paths (
/users/:userId/settings), not cryptic ones (/u/:id/s) - Add
endto aNavLink to="/"so it isn't highlighted on every page
❌ Don't
- Use
<a href>for internal navigation — it reloads the page and destroys app state - Reach back to the old v5
<Switch>/component=API — use v6+<Routes>/element= - Nest a second
BrowserRouterinside your app - Put secrets or huge blobs in the URL — use route
statefor transient data instead
⚠️ The refresh-404 gotcha
Because BrowserRouter uses real paths, refreshing on /about asks the server for /about — which may not exist as a file. In production you must configure the host to serve index.html for all routes (a "SPA fallback"). On Netlify that's a one-line _redirects file: /* /index.html 200.
Summary
🎉 Key Takeaways
- Client-side routing swaps components in place — the URL changes, but the page never reloads
- React Router uses the browser History API to change the URL and then matches it to a
<Route> - Wrap the app once in
BrowserRouter; declare screens with<Routes>+<Route element> - Navigate with
<Link>, highlight the active page with<NavLink>, and never with a bare<a> - Read dynamic segments with
useParamsand redirect in code withuseNavigate
📚 Additional Resources
- React Router — Routing (official docs)
- React Router — Navigating with Link and NavLink
- MDN — The History API
🚀 What's Next?
You now understand why routing works and can wire up basic screens. Next we get organized: the Route Configuration lesson shows how to define routes as data with createBrowserRouter, unlocking loaders, error boundaries, and nested layouts.
🎉 Great start!
One HTML file, many screens — you've just unlocked the single-page-app superpower.