Session 4 — Client-Side Routing with React Router
Real apps have multiple pages. Today we master React Router — the standard library for client-side navigation in React — from setup to advanced nested routes.
How the browser History API enables page-like navigation without reloads.
Install, BrowserRouter, Routes, and Route — the three building blocks.
Client-side navigation components and auto-active styles for menus.
Dynamic routes with :id, useParams, and query strings with useSearchParams.
useNavigate for redirecting after actions like login or form submit.
Shared layouts, Outlet, and organizing complex route trees.
Review key concepts and test what you've learned.
Before React Router, every navigation click sent a request to the server, which returned a completely new HTML page. React apps work differently — they are Single-Page Applications.
React Router uses the browser's built-in History API (history.pushState) to change the URL without reloading the page. It then reads the current URL and renders the matching component.
The URL is just another piece of application state. When it changes, the right component re-renders.
Navigation happens entirely in the browser. Only data requests go to the server.
Users can bookmark and share URLs like /users/42 — React Router recreates the right view.
The browser history stack is real. The back and forward buttons navigate your app correctly.
React Router is a separate package. Install it, then wrap your app in BrowserRouter and define your routes with Routes and Route.
npm install react-router-dom
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( <BrowserRouter> {/* ← wrap once here, at the top */} <App /> </BrowserRouter> )
import { Routes, Route } from 'react-router-dom' import HomePage from './pages/HomePage' import AboutPage from './pages/AboutPage' import UserPage from './pages/UserPage' import NotFound from './pages/NotFound' function App() { return ( <Routes> <Route path="/" element={<HomePage />} /> <Route path="/about" element={<AboutPage />} /> <Route path="/users/:id" element={<UserPage />} /> <Route path="*" element={<NotFound />} /> </Routes> ) }
Provides routing context to your entire app. Goes in main.jsx, wrapping <App>. Never nest a second BrowserRouter inside.
Looks at the current URL and renders only the first matching Route. No need for the old "exact" keyword — v6 matches exactly by default.
Maps a URL pattern to a component. path="/users/:id" captures dynamic segments; path="*" is the catch-all 404 route.
| path pattern | Matches | Does NOT match |
|---|---|---|
"/" |
/ | /about, /users |
"/about" |
/about | /about/team |
"/users/:id" |
/users/1, /users/42 | /users, /users/1/posts |
"*" |
anything unmatched | — |
Use <Link> instead of <a href> for all internal navigation. A regular anchor tag triggers a full page reload — Link keeps it client-side.
A regular <a href="/about"> causes a full page reload — React loses all state and re-downloads the JavaScript bundle. Always use <Link to="/about"> inside React Router.
import { Link, NavLink } from 'react-router-dom' function Navbar() { return ( <nav> {/* Link — basic client-side navigation */} <Link to="/">Home</Link> {/* NavLink — same as Link but adds className="active" when matched */} <NavLink to="/about">About</NavLink> {/* Custom active style with a function */} <NavLink to="/dashboard" className={({ isActive }) => isActive ? 'nav-active' : ''} style={({ isActive }) => isActive ? { color: '#61dafb' } : {}} > Dashboard </NavLink> </nav> ) }
| Element | Client-side? | Auto active class? | Use when |
|---|---|---|---|
<a href> |
No — full reload | No | External links only |
<Link> |
Yes | No | Any internal link |
<NavLink> |
Yes | Yes — adds active |
Navigation menus & tabs |
Click the nav items to simulate client-side routing (no page reload):
—Real apps have dynamic URLs — /users/42, /posts/react-basics. Define the dynamic segment with a colon :param, then read it with the useParams hook.
import { useParams } from 'react-router-dom' // Route definition: <Route path="/users/:id" element={<UserPage />} /> function UserPage() { const { id } = useParams() // URL /users/42 → id = "42" (always a string) // URL /users/ahmed → id = "ahmed" return <h2>Viewing user {id}</h2> } // Multiple params: /posts/:category/:slug function PostPage() { const { category, slug } = useParams() // /posts/react/hooks-tutorial → category="react", slug="hooks-tutorial" return <p>{category} / {slug}</p> }
Even if the URL has a number like /users/42, useParams() returns it as the string "42". Parse it if you need a number: const numId = Number(id).
Query strings are the ?key=value part of a URL — /search?q=react&page=2. Use useSearchParams to read and update them.
import { useSearchParams } from 'react-router-dom' function SearchPage() { const [searchParams, setSearchParams] = useSearchParams() // Read: /search?q=react&page=2 const query = searchParams.get('q') // "react" const page = searchParams.get('page') // "2" function goToPage3() { // Write: updates URL to /search?q=react&page=3 setSearchParams({ q: query, page: '3' }) } return ( <div> <p>Searching for: {query} (page {page})</p> <button onClick={goToPage3}>Next Page</button> </div> ) }
| Hook | Reads from | Example URL | Example value |
|---|---|---|---|
useParams() |
URL path segments | /users/42 |
id = "42" |
useSearchParams() |
Query string | /search?q=react |
get('q') = "react" |
Nested routes let child routes render inside a parent layout — perfect for dashboards, settings pages, or any UI where a section of the page stays fixed while a sub-section changes.
<Outlet />import { Routes, Route } from 'react-router-dom' function App() { return ( <Routes> <Route path="/" element={<HomePage />} /> {/* Parent route — renders DashboardLayout */} <Route path="/dashboard" element={<DashboardLayout />}> {/* index — matches /dashboard exactly */} <Route index element={<Overview />} /> {/* child routes — match /dashboard/settings, /dashboard/profile */} <Route path="settings" element={<Settings />} /> <Route path="profile" element={<Profile />} /> </Route> <Route path="*" element={<NotFound />} /> </Routes> ) }
import { Outlet, NavLink } from 'react-router-dom' function DashboardLayout() { return ( <div className="dashboard"> <aside> {/* Sidebar stays mounted for all child routes */} <NavLink to="/dashboard" end>Overview</NavLink> <NavLink to="/dashboard/settings">Settings</NavLink> <NavLink to="/dashboard/profile">Profile</NavLink> </aside> <main> <Outlet /> {/* child page renders HERE */} </main> </div> ) }
Without end, the /dashboard link would stay "active" on /dashboard/settings too (because /dashboard is a prefix). The end prop tells NavLink to only match the exact path.
<Route index /> renders when the parent path matches exactly. For /dashboard it shows Overview; navigating to /dashboard/settings replaces it with Settings.
Inside a nested route, you write path="settings", not path="/dashboard/settings". React Router prepends the parent path automatically.
The parent layout component must render <Outlet /> wherever it wants child pages to appear. Without it, child routes will match but show nothing.
Let's review the key concepts from today's session.
The three building blocks. BrowserRouter wraps the app once; Routes + Route map URLs to components.
Use Link for internal navigation (never <a href>). NavLink auto-applies the active class for menus.
Read dynamic path segments (:id) with useParams. Read query strings (?q=...) with useSearchParams.
Redirect programmatically with useNavigate. Share layouts across child routes with Outlet.
<Link to="/"> instead of <a href="/"> inside a React app?path="/products/:id" and the URL /products/99, what does useParams() return?In Session 5, we master forms & validation — controlled inputs for every input type, multi-field forms, submit handling, validation patterns, and real-time user feedback.