Skip to main content

🧭 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 useParams and navigate in code with useNavigate

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.

graph LR A[Browser URL] --> B[React Router] B --> C{Route Matcher} C -->|/| D[Home] C -->|/about| E[About] C -->|/products| F[Products] C -->|no match| G[404 Not Found]

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 reloads the whole page; client-side routing swaps a component in place Server-side Click link Request whole page Full page reload ⚠️ blank flash, lose state Client-side Click Link Router updates URL Swap component ✅ instant, keeps state
The server path throws away the whole page and rebuilds it; the client path only re-renders the piece that changed.
Server-side routingClient-side routing
Who decides the page?The server, per requestThe router, in the browser
Network request per navigation?Yes — a full HTML documentNo — data only, if any
Page reload?Full reload every timeNone — content swaps in place
FeelClassic websiteApp-like, instant
First loadFast (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.

sequenceDiagram participant U as User participant L as Link participant R as Router participant H as History API participant C as Component U->>L: Click "About" L->>H: pushState('/about') H-->>R: URL is now /about R->>R: Match /about to About route R->>C: Render About C-->>U: New screen (no reload)

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 BrowserRouter exactly once, at the top of the tree
  • Use <Link>/<NavLink> for user clicks and useNavigate for 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 end to a NavLink 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 BrowserRouter inside your app
  • Put secrets or huge blobs in the URL — use route state for 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 useParams and redirect in code with useNavigate

📚 Additional Resources

🚀 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.