Topic 8: React Router
📖 11 min read · 🎯 intermediate · 🧭 Prerequisites: events, react-lists
Why this matters
Up until now, every React app you've built lives on a single page — no matter what you click, the URL never changes. That feels fine for small demos, but imagine handing someone a link to your app's "About" section and it just opens the homepage. Frustrating, right? React Router fixes exactly that. It's the library that lets you tie specific URLs — /home, /about, /products/42 — to specific components, so your app feels like a real multi-page website. That's what react-router-dom gives you, and that's what we're building today.
What You'll Learn
- Install and configure
react-router-domin a React application - Build declarative routes using
BrowserRouter,Route,Switch, andLink - Compose nested routes with
useRouteMatchanduseParams - Navigate programmatically with the
useHistoryhook - Protect routes with a
PrivateRoutepattern andRedirect
The Analogy
Think of React Router as a city's postal system. Your app is the city, each component is a building, and every URL is a street address. The BrowserRouter is the postmaster who reads the current address bar; Route entries are the buildings registered at specific addresses; Link components are the signposts that tell citizens where to go next; and Switch is the routing algorithm that delivers mail to exactly one building rather than broadcasting to all of them. When you want to forward mail automatically — say, because a building is restricted — you use a Redirect, which is exactly like returning a letter stamped "Forward to: /" .
Chapter 1: Introduction to React Router
React Router is the standard library for routing in React applications. It enables navigation among views of various components, allows changing the browser URL, and — crucially — keeps the UI in sync with the URL at all times.
Key responsibilities React Router handles:
- Matching the current URL to a component
- Rendering that component into the page
- Updating the URL without a full browser reload
- Providing hooks so components can read route parameters and navigate programmatically
Chapter 2: Setting Up React Router
Start by scaffolding a fresh React app and installing react-router-dom.
npx create-react-app react-router-example
cd react-router-example
npm install react-router-dom
react-router-dom is the DOM-specific binding of React Router. It re-exports everything from react-router and adds browser-focused components like BrowserRouter and Link.
Chapter 3: Basic Routing
the trainer walked the class through the three primitives that underpin every React Router setup: BrowserRouter, Route, and Link.
| Primitive | Role |
|---|---|
BrowserRouter | Wraps the app; listens to the browser history API |
Route | Declares which component renders at which path |
Switch | Renders only the first Route that matches |
Link | Renders an <a> tag that updates the URL without reload |
src/App.js — wiring up three basic routes:
import React from 'react';
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';
import Home from './components/Home';
import About from './components/About';
import Contact from './components/Contact';
function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/contact">Contact</Link>
</li>
</ul>
</nav>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
</Switch>
</div>
</Router>
);
}
export default App;
Notice the exact prop on the "/" route. Without it, React Router would match / for every path (since all paths start with /), and Home would always render.
src/components/Home.js:
import React from 'react';
function Home() {
return (
<div>
<h1>Home Page</h1>
<p>Welcome to the Home page!</p>
</div>
);
}
export default Home;
src/components/About.js:
import React from 'react';
function About() {
return (
<div>
<h1>About Page</h1>
<p>Learn more about us on this page.</p>
</div>
);
}
export default About;
src/components/Contact.js:
import React from 'react';
function Contact() {
return (
<div>
<h1>Contact Page</h1>
<p>Get in touch with us on this page.</p>
</div>
);
}
export default Contact;
Chapter 4: Nested Routing
Real applications need hierarchy — a /topics page that itself renders sub-routes like /topics/rendering or /topics/components. React Router handles this with the useRouteMatch hook, which returns the matched URL and path for the currently active route so child components can build relative links without hardcoding parent paths.
graph TD
A["/"] --> B[Home]
A --> C["/about"]
A --> D["/contact"]
A --> E["/topics"]
E --> F["/topics/rendering"]
E --> G["/topics/components"]
E --> H["/topics/props-v-state"]
src/components/Topics.js — the parent that owns its own nested Route declarations:
import React from 'react';
import { Route, Link, useRouteMatch } from 'react-router-dom';
import Topic from './Topic';
function Topics() {
let match = useRouteMatch();
return (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${match.url}/rendering`}>Rendering with React</Link>
</li>
<li>
<Link to={`${match.url}/components`}>Components</Link>
</li>
<li>
<Link to={`${match.url}/props-v-state`}>Props v. State</Link>
</li>
</ul>
<Route path={`${match.path}/:topicId`} component={Topic} />
<Route exact path={match.path}>
<h3>Please select a topic.</h3>
</Route>
</div>
);
}
export default Topics;
match.url produces the actual matched URL string (used in Link hrefs), while match.path is the route pattern (used in Route path props). They look the same for static routes but differ when parameters are involved.
src/components/Topic.js — reads the dynamic segment with useParams:
import React from 'react';
import { useParams } from 'react-router-dom';
function Topic() {
let { topicId } = useParams();
return (
<div>
<h3>{topicId}</h3>
</div>
);
}
export default Topic;
useParams returns an object whose keys match the :paramName segments defined in the parent route path. Here, :topicId becomes topicId.
src/App.js — updated to include the Topics route:
import React from 'react';
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';
import Home from './components/Home';
import About from './components/About';
import Contact from './components/Contact';
import Topics from './components/Topics';
function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/contact">Contact</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>
</nav>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
<Route path="/topics" component={Topics} />
</Switch>
</div>
</Router>
);
}
export default App;
Chapter 5: Programmatic Navigation
Sometimes navigation should be triggered by logic — a form submission, an API response, or a button click — rather than a <Link> click. The useHistory hook gives you direct access to the browser history object.
src/components/NavigateButton.js:
import React from 'react';
import { useHistory } from 'react-router-dom';
function NavigateButton() {
let history = useHistory();
function handleClick() {
history.push('/about');
}
return (
<button type="button" onClick={handleClick}>
Go to About
</button>
);
}
export default NavigateButton;
history.push(path) adds a new entry to the browser history stack and navigates there. Use history.replace(path) instead when you don't want the user to be able to press the back button to return (e.g., after logging out).
src/App.js — integrating NavigateButton:
import React from 'react';
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';
import Home from './components/Home';
import About from './components/About';
import Contact from './components/Contact';
import Topics from './components/Topics';
import NavigateButton from './components/NavigateButton';
function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/contact">Contact</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
</ul>
</nav>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
<Route path="/topics" component={Topics} />
</Switch>
<NavigateButton />
</div>
</Router>
);
}
export default App;
Chapter 6: Redirects and Navigation Guards
Not all routes should be publicly accessible. The PrivateRoute pattern wraps Route and inspects an authentication condition before deciding whether to render the target component or redirect the user elsewhere using React Router's Redirect component.
src/components/PrivateRoute.js:
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
function PrivateRoute({ component: Component, ...rest }) {
const isAuthenticated = false; // Replace with actual authentication logic
return (
<Route
{...rest}
render={(props) =>
isAuthenticated ? <Component {...props} /> : <Redirect to="/" />
}
/>
);
}
export default PrivateRoute;
PrivateRoute accepts the same props as Route (including path and exact). It spreads ...rest onto the inner Route and uses the render prop — a function that receives route props — to conditionally render the component or issue a Redirect. When isAuthenticated is false, any attempt to visit the protected path instantly sends the user back to "/".
src/components/Secret.js — the protected component:
import React from 'react';
function Secret() {
return (
<div>
<h1>Secret Page</h1>
<p>This is a secret page only accessible to authenticated users.</p>
</div>
);
}
export default Secret;
src/App.js — the final version with PrivateRoute, Secret, and NavigateButton all wired in:
import React from 'react';
import { BrowserRouter as Router, Route, Switch, Link } from 'react-router-dom';
import Home from './components/Home';
import About from './components/About';
import Contact from './components/Contact';
import Topics from './components/Topics';
import PrivateRoute from './components/PrivateRoute';
import Secret from './components/Secret';
import NavigateButton from './components/NavigateButton';
function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/contact">Contact</Link>
</li>
<li>
<Link to="/topics">Topics</Link>
</li>
<li>
<Link to="/secret">Secret</Link>
</li>
</ul>
</nav>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
<Route path="/topics" component={Topics} />
<PrivateRoute path="/secret" component={Secret} />
</Switch>
<NavigateButton />
</div>
</Router>
);
}
export default App;
🧪 Try It Yourself
Task: Add a user profile route that is only accessible when isAuthenticated is true, and test the redirect behavior by toggling the flag.
- Create
src/components/Profile.jsthat renders a heading and a paragraph with any user info you like. - Create a second
PrivateRouteinApp.jsat path"/profile"pointing toProfile. - Add a
<Link to="/profile">Profile</Link>in the nav. - Run the app with
npm start.
With isAuthenticated = false: clicking "Profile" should immediately redirect you to "/".
With isAuthenticated = true: clicking "Profile" should render the Profile component.
Starter snippet for Profile.js:
import React from 'react';
function Profile() {
return (
<div>
<h1>Profile Page</h1>
<p>Welcome back, students.</p>
</div>
);
}
export default Profile;
Success criterion: toggling isAuthenticated between false and true in PrivateRoute.js should switch the /profile route between redirecting and rendering — without touching any other file.
🔍 Checkpoint Quiz
Q1. Why is the exact prop required on the "/" route when using Switch?
A) Because / is not a valid path without it
B) Because without it, "/" matches every path (all paths begin with /), so Home would always render
C) Because Switch ignores paths shorter than two characters by default
D) Because BrowserRouter requires exact on root routes
Q2. Given this component, what happens when a user visits /topics/hooks in the browser?
function Topics() {
let match = useRouteMatch();
return (
<div>
<Route path={`${match.path}/:topicId`} component={Topic} />
</div>
);
}
function Topic() {
let { topicId } = useParams();
return <h3>{topicId}</h3>;
}
A) The page renders nothing — useRouteMatch returns null outside a Switch
B) The page renders <h3>hooks</h3>
C) The page renders <h3>/topics/hooks</h3>
D) React throws an error because useParams requires an explicit param name
Q3. What is the difference between history.push('/about') and history.replace('/about') in the useHistory hook?
A) push is asynchronous; replace is synchronous
B) push adds a new history entry (back button works); replace overwrites the current entry (back button skips it)
C) push navigates within a Switch; replace navigates outside it
D) There is no functional difference — both perform the same navigation
Q4. You have a dashboard route that should only be visible to logged-in users. Unauthenticated visitors should land on /login. How would you adapt the PrivateRoute pattern to redirect to /login instead of "/"?
(Open-ended — write the updated Redirect line.)
A1. B — Without exact, React Router treats "/" as a prefix match. Every URL starts with /, so Home would render for /about, /contact, and every other path. The exact prop restricts the match to the full string "/" only.
A2. B — useRouteMatch provides the matched URL/path for the /topics route. The inner Route pattern becomes /topics/:topicId, which matches /topics/hooks and extracts "hooks" as topicId. useParams returns { topicId: "hooks" }, so the component renders <h3>hooks</h3>.
A3. B — history.push appends a new entry to the browser history stack, so the user can press the back button to return. history.replace overwrites the current stack entry, so the previous page is no longer reachable via the back button. Use replace for post-logout redirects or wizard step navigation where going "back" would be confusing.
A4. Change <Redirect to="/" /> to <Redirect to="/login" /> inside PrivateRoute:
isAuthenticated ? <Component {...props} /> : <Redirect to="/login" />
🪞 Recap
react-router-domprovidesBrowserRouter,Route,Switch, andLinkas the four core primitives for declarative SPA routing.- The
exactprop on the root"/"route prevents it from matching every other path inside aSwitch. - Nested routes are built by calling
useRouteMatchinside a child component to get relativeurlandpathvalues. useParamsextracts dynamic URL segments (:topicId) as a plain object inside any matched route component.useHistoryenables programmatic navigation viahistory.push(adds history entry) orhistory.replace(overwrites it).- The
PrivateRoutepattern wrapsRoutewith an auth check and issues aRedirectto guard protected pages.
📚 Further Reading
- React Router official docs — the source of truth for all API details and migration guides
- React Router v5 API reference — full reference for the v5 API used in this lesson
- React Router: Hooks overview — deep dive into
useHistory,useParams,useLocation, anduseRouteMatch - ⬅️ Previous: React Lists
- ➡️ Next: Hooks