Level 5: React Router – Building Single Page Applications (SPAs)
Core React ends once hooks and local state management are mastered. To build real-world, production-ready web applications, developers must master third-party ecosystem tools—starting with React Router. This chapter covers client-side routing, single-page application (SPA) architecture, layout nesting via <Outlet />, navigation primitives (<Link> vs. <NavLink>), and both object-based and JSX-based routing declarations using react-router-dom.
1. The Shift to SPAs: Multi-Page vs. Single-Page Routing
In traditional multi-page web applications, navigation relies on standard HTML anchor tags (<a href="...">). Every time a user clicks a link, the browser executes a full page lifecycle:
In contrast, React applications mount inside a single root container in the DOM:
<div id="root"></div>
Reloading the entire HTML document on every page transition destroys in-memory state and creates visual flicker.
The Single Page Application (SPA) Mental Model
A Single Page Application loads a single HTML file once. When users navigate between URLs (e.g., / to /about to /contact):
- The browser prevents the default server round-trip.
- The URL in the address bar updates via the browser's History API (
pushState). - React Router detects the path change, unmounts the old component, and mounts the new component into the existing DOM tree—instantly and without a page reload.
2. Core Mental Model: Layout Shells and the <Outlet />
When designing modern web layouts, major UI regions remain constant while only central content changes:
Instead of re-rendering the <Navbar /> and <Footer /> across separate pages, React Router provides the <Outlet /> component.
- The layout component (
App.jsx) acts as a shell rendering persistent structural elements. - The
<Outlet />functions as an internal placeholder. - When the active URL matches a child route, React Router dynamically injects that child route's component directly into the
<Outlet />slot.
3. Project Initialization & Environment Setup
Step 1: Scaffold Vite Project
Create a lightweight React project using Vite:
npm create vite@latest react-router -- --template react
cd react-router
npm install
Step 2: Install React Router DOM
react-router-dom is the official package containing browser-specific bindings for React Router:
npm install react-router-dom
Step 3: Project Cleanup
Strip out standard boilerplate files:
- Clear the default styling inside
src/index.cssandsrc/App.css. - Clean
src/App.jsxto prepare it as the primary layout wrapper.
4. Building the Persistent Shell
Create a dedicated src/components/ directory with the following structure:
1. The Shell Component (src/App.jsx)
The root application component imports <Outlet /> from react-router-dom. It pins the navigation bar to the top and the footer to the bottom, rendering dynamic page components in between.
import Navbar from './components/Navbar';
import Footer from './components/Footer';
import { Outlet } from 'react-router-dom';
import './App.css';
function App() {
return (
<>
<Navbar />
<Outlet />
<Footer />
</>
);
}
export default App;
2. The Persistent Footer (src/components/Footer.jsx)
A simple static footer rendering a copyright symbol via the HTML entity ©:
import React from 'react';
function Footer() {
return (
<footer className="footer">
Copyright © All Rights Reserved
</footer>
);
}
export default Footer;
3. Page Components (Home.jsx, About.jsx, Contact.jsx)
Each child component represents a distinct view to mount inside the <Outlet />:
// src/components/Home.jsx
export default function Home() {
return <div className="page home">Home Page Content</div>;
}
// src/components/About.jsx
export default function About() {
return <div className="page about">About Page Content</div>;
}
// src/components/Contact.jsx
export default function Contact() {
return <div className="page contact">Contact Page Content</div>;
}
5. Route Configuration Method 1: Objects via createBrowserRouter
Modern React Router (v6.4+) recommends configuring routes as JavaScript objects passed into createBrowserRouter.
Implementing Nested Hierarchy
In src/main.jsx:
- Define a parent route mapped to the root path
/. - Assign
element: <App />to render the persistent layout. - Define nested views inside a
childrenarray.
import React from 'react';
import ReactDOM from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import App from './App';
import Home from './components/Home';
import About from './components/About';
import Contact from './components/Contact';
import './index.css';
const router = createBrowserRouter([
{
path: '/',
element: <App />,
children: [
{
path: '',
element: <Home />,
},
{
path: 'about',
element: <About />,
},
{
path: 'contact',
element: <Contact />,
},
],
},
]);
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
Note: An empty string child path (
path: '') matches the parent URL (/) exactly, making<Home />the default index component rendered inside the<Outlet />.
6. Client-Side Navigation: <a> vs. <Link> vs. <NavLink>
HTML anchor tags (<a href="/about">) must never be used for internal navigation in a React Router SPA because they cause a full browser reload.
React Router provides two primary primitives:
Deep Dive: <Link> vs. <NavLink>
7. Dynamic Navigation Bar with <NavLink>
When using <NavLink>, React Router automatically appends the CSS class active to the generated DOM element if its to prop matches the current path.
Implementation (src/components/Navbar.jsx)
import React from 'react';
import { NavLink } from 'react-router-dom';
function Navbar() {
return (
<nav className="navbar">
<div className="logo">DevBrand</div>
<ul className="nav-links">
<li>
<NavLink to="/">Home</NavLink>
</li>
<li>
<NavLink to="/about">About</NavLink>
</li>
<li>
<NavLink to="/contact">Contact</NavLink>
</li>
</ul>
</nav>
);
}
export default Navbar;
8. Styling Active Links via the DOM
Inspect the browser elements to see how <NavLink> operates in the real DOM:
<!-- When URL is http://localhost:5173/about -->
<ul class="nav-links">
<li><a href="/" class="">Home</a></li>
<li><a href="/about" class="active">About</a></li>
<li><a href="/contact" class="">Contact</a></li>
</ul>
Because <NavLink> compiles down to a native <a> tag, CSS rules must target a and a.active. Attempting to style navlink { ... } in CSS will fail because <NavLink> is a React abstraction, not an HTML element.
Styling Setup (src/App.css)
/* Universal Base Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Gill Sans', sans-serif;
}
/* Navbar Layout */
.navbar {
width: 100%;
height: 15vh;
background-color: #111111;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 40px;
color: #ffffff;
}
.nav-links {
display: flex;
align-items: center;
list-style: none;
gap: 20px;
}
/* Navigation Anchor Defaults */
.nav-links a {
text-decoration: none;
color: #ffffff;
font-size: 20px;
transition: color 0.2s ease-in-out;
}
/* Dynamic Active Class injected by NavLink */
.nav-links a.active {
color: #a0522d; /* Highlight color: Sienna Brown */
font-weight: bold;
}
/* Dynamic Middle Component (Outlet Target) */
.page {
width: 100%;
height: 70vh;
display: flex;
align-items: center;
justify-content: center;
font-size: 8vw;
color: #f0f8ff;
}
.home {
background-color: #2c3e50;
}
.about {
background-color: #16a085;
}
.contact {
background-color: #8e44ad;
}
/* Persistent Footer */
.footer {
width: 100%;
height: 15vh;
background-color: #1a1a1a;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: #f0f8ff;
}
9. Route Configuration Method 2: JSX with createRoutesFromElements
In addition to plain JavaScript object arrays, React Router supports an alternative declarative syntax using createRoutesFromElements and <Route> components. Both methods compile to identical internal route manifests.
Implementation (src/main.jsx Alternative)
import React from 'react';
import ReactDOM from 'react-dom/client';
import {
createBrowserRouter,
createRoutesFromElements,
Route,
RouterProvider,
} from 'react-router-dom';
import App from './App';
import Home from './components/Home';
import About from './components/About';
import Contact from './components/Contact';
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<App />}>
<Route path="" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="contact" element={<Contact />} />
</Route>
)
);
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
10. Comparing Configuration Strategies
11. Complete Navigation Lifecycle Flow
12. Summary and Key Takeaways
- React Core vs. Router: Core React handles components, state, and hooks; third-party packages like
react-router-domprovide application-level navigation and SPA architecture. - The True Meaning of an SPA: Only one HTML document is loaded. Component swapping happens client-side without full-page reloads, network white-flashes, or state loss.
- Persistent Shell with
<Outlet />: Shared layouts (e.g., headers, sidebars, footers) live in parent route components, while variable views mount into<Outlet />. - Never Use Anchor Tags: Traditional
<a href="...">tags force server-side page reloads. Use<Link>or<NavLink>instead. <NavLink>Automatic Active State:<NavLink>automatically inspects the URL and injects.activeinto the rendered HTML<a>tag, simplifying active tab styling.- Target
ain CSS: Because<NavLink>renders as a native HTML<a>element, write CSS selectors targetingaanda.active. - Two Flexible Setup Syntaxes: Modern React Router supports both plain object arrays (
createBrowserRouter([...])) and JSX structures (createRoutesFromElements(<Route ... />)) with identical runtime performance.