Routing Fundamentals — Client-Side Navigation in SPAs

A traditional multi-page website sends a new HTML document from the server on every click. A Single Page Application (SPA) loads one HTML document and then updates the page content using JavaScript — the server is only asked for data (JSON from the API), never for new pages. React Router gives React SPAs the appearance of multiple pages by mapping URL paths to different components, using the browser’s History API to update the URL without triggering a full page load. The result: fast, app-like navigation with shareable, bookmarkable URLs.

How Client-Side Routing Works

Traditional server-side routing:
  User clicks /posts/42
  → Browser sends GET /posts/42 to server
  → Server returns a new HTML page
  → Full page reload, all JS/CSS re-downloaded

Client-side routing (React Router):
  User clicks /posts/42
  → React Router intercepts the click (e.preventDefault())
  → URL bar updates to /posts/42 via History API (pushState)
  → React Router renders the <PostDetailPage> component
  → No server request, no page reload — instant navigation
  → Components fetch data from /api/posts/42 as needed

On direct URL access (/posts/42):
  → Server returns index.html (the SPA shell)
  → React loads, React Router reads the URL
  → Renders <PostDetailPage> directly
  (Requires server configuration: serve index.html for all routes)
Note: React Router v6 introduced a significant API rewrite from v5. The course uses React Router v6 (react-router-dom v6.x). The key changes: Switch is replaced by Routes, route matching is exact by default, nested routes use Outlet, and useHistory is replaced by useNavigate. If you find older tutorials using Switch or component={PostCard} props, those are v5 patterns that will not work in v6.
Tip: For the Vite dev server, no extra configuration is needed — Vite serves index.html for all unmatched routes by default. For production deployment, configure your web server (Nginx, Apache) to serve index.html for all routes: try_files $uri $uri/ /index.html; in Nginx. Without this, a user who bookmarks /posts/42 and visits directly will get a 404 from the server because the server does not have a /posts/42 file.
Warning: Never use regular <a href="..."> tags for internal navigation in a React Router application. An <a> tag causes a full page reload, destroying all React state and re-downloading the application. Always use React Router’s <Link to="..."> or <NavLink to="..."> for internal links. External links (other websites) still use regular <a> tags with target="_blank" rel="noopener noreferrer".

Installation

npm install react-router-dom
// src/main.jsx — wrap the app with BrowserRouter
import { StrictMode }    from "react";
import { createRoot }    from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App               from "./App.jsx";
import "./index.css";

createRoot(document.getElementById("root")).render(
    <StrictMode>
        <BrowserRouter>
            <App />
        </BrowserRouter>
    </StrictMode>
);
// BrowserRouter provides the routing context that all Router hooks need
// It must wrap any component that uses useNavigate, useParams, Link, etc.

Router Types

Router URL Format Server Needed Use For
BrowserRouter /posts/42 Yes (serve index.html) Production SPAs
HashRouter /#/posts/42 No (hash is client-only) Static hosting without server config
MemoryRouter Not visible No Tests, React Native

Common Mistakes

❌ Wrong — full page reload, loses all React state:

<a href="/posts/42">Read Post</a>   // causes full page reload!

✅ Correct — React Router’s Link for internal navigation:

import { Link } from "react-router-dom";
<Link to="/posts/42">Read Post</Link>   // ✓ no page reload

Mistake 2 — Not wrapping the app with BrowserRouter

❌ Wrong — hooks throw: “useNavigate may be used only in the context of a Router”:

createRoot(document.getElementById("root")).render(<App />);   // no BrowserRouter!

✅ Correct — wrap with BrowserRouter in main.jsx.

Quick Reference

Concept Details
Install npm install react-router-dom
Wrap app <BrowserRouter><App /></BrowserRouter> in main.jsx
Internal link <Link to="/path"> (not <a href>)
Active link <NavLink to="/path" className={({isActive}) => ...}>
URL params const { id } = useParams()
Navigate const navigate = useNavigate(); navigate("/path")

🧠 Test Yourself

A user bookmarks https://blog.example.com/posts/42. The app uses BrowserRouter but the production Nginx server is not configured to serve index.html for all paths. What happens when they visit the bookmark?