Lesson 7 • Intermediate
React Router ⚛️
By the end of this lesson you'll be able to turn a single React component into a multi-page app — defining routes, linking between pages without reloads, reading data out of the URL, redirecting in code, sharing layouts with nested routes, and showing a proper 404 page.
What You'll Learn
- Switch on routing by wrapping your app in BrowserRouter
- Map URLs to components with Routes and Route (plus a 404 with path="*")
- Navigate without full reloads using Link, and highlight the current page with NavLink
- Read dynamic URL values like /users/42 with useParams
- Redirect from code (e.g. after login) with useNavigate
- Share a layout across pages using nested routes and Outlet
useState hook. React Router isn't built into React — in a real project you install it with npm install react-router-dom (this lesson uses v6).:userId is a room whose occupant changes — same door, different person inside. And path="*" is the "you've wandered into a wall" sign: the 404 page.useParams actually does.1. Setup & Basic Routes
A router's one job is to look at the current URL and show the matching component. You switch routing on by wrapping your whole app in <BrowserRouter> once (usually in main.jsx). Then you list your pages inside <Routes>, one <Route> per page, each with a path and the element to render. Read this worked example carefully — every line is commented.
// main.jsx — wrap your WHOLE app in <BrowserRouter> exactly once.
import { BrowserRouter } from 'react-router-dom';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<BrowserRouter> {/* turns on client-side routing for everything inside */}
<App />
</BrowserRouter>
);
// App.jsx — map URL paths to components with <Routes> and <Route>.
import { Routes, Route } from 'react-router-dom';
function App() {
return (
<Routes>
{/* path="/" -> the homepage; element is the component to show */}
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
{/* path="*" is the catch-all 404 — it MUST come last */}
<Route path="*" element={<NotFound />} />
</Routes>
);
}
// Visiting /about renders <About />. Visiting /banana renders <NotFound />.The special path="*" route matches any URL no other route caught, so it's your 404 page — and it goes last. Now run the logic a router uses to pick a route. This is plain JS you can execute:
Worked example: how route matching works
Run it and watch /banana fall through to the wildcard 404.
// === How a router decides which page to show ===
// React Router compares the current URL against each route's pattern,
// top to bottom, and renders the FIRST one that matches.
const routes = [
{ path: "/", page: "Home" },
{ path: "/about", page: "About" },
{ path: "/contact", page: "Contact" },
{ path: "*", page: "NotFound" }, // catch-all, kept last
];
// A tiny matcher: "*" matches anything, otherwise it's an exact match.
function matchRoute(url) {
for (const
...Your turn. The matcher below is almost done — fill in the one blank so unknown URLs fall through to the 404, then run it and check the output.
🎯 Your turn: finish the route matcher
Replace ___ with the catch-all string, then run.
// 🎯 YOUR TURN — replace each ___ then press "Run".
// Goal: finish the matcher so unknown URLs fall through to the 404 page.
const routes = [
{ path: "/", page: "Home" },
{ path: "/shop", page: "Shop" },
{ path: "*", page: "NotFound" },
];
function matchRoute(url) {
for (const route of routes) {
// 1) The wildcard "*" should match ANY url. Compare route.path to it.
if (route.path === ___ || route.path === url) { // 👉 the catch-all string
return route.page;
...2. Linking Between Pages
Inside a routed app you must never use a plain <a href="..."> to move between your own pages — that makes the browser throw your app away and reload the whole thing from the server, losing all React state. Use <Link to="..."> instead: it swaps the page in JavaScript, instantly. <NavLink> is the same thing but it knows when it points at the current page, so you can style the active link.
// Navbar.jsx — navigate WITHOUT a full page reload.
import { Link, NavLink } from 'react-router-dom';
function Navbar() {
return (
<nav>
{/* <Link> swaps the page in JavaScript — React state survives. */}
<Link to="/">Home</Link>
<Link to="/about">About</Link>
{/* <NavLink> is a <Link> that KNOWS if it's the current page.
The className function gives you { isActive } to style with. */}
<NavLink
to="/contact"
className={({ isActive }) =>
isActive ? 'font-bold text-blue-600' : 'text-gray-600'
}
>
Contact
</NavLink>
</nav>
);
}
// On /contact the Contact link is bold+blue; the others stay grey.
// Never write <a href="/about"> here — that reloads the whole app.🔎 Deep Dive: why <a> is so bad here
A normal anchor tag tells the browser "go fetch this URL from the server". The browser tears down the current page, downloads everything again, and re-runs your whole React app from scratch — so any state (a half-filled form, a scroll position, a logged-in flag held in memory) vanishes, and there's a visible flash.
<Link> intercepts the click, updates the URL bar with the History API, and just renders the new component. No server round-trip, no flash, no lost state. That speed is the entire point of a single-page app.
3. Dynamic Routes & URL Params
You don't write one route per user. Instead you put a placeholder in the path with a colon — /users/:userId — and that :userId matches whatever's in that slot. Inside the component you read it with the useParams() hook. The key you destructure must match the name after the colon, and the value always comes back as a string.
// 1) Declare a dynamic segment with a colon: :userId is a placeholder.
<Route path="/users/:userId" element={<UserProfile />} />
// 2) Read it inside the component with useParams().
import { useParams } from 'react-router-dom';
function UserProfile() {
const { userId } = useParams(); // key MUST match the route: ":userId"
// URL /users/42 -> userId is "42" (always a string!)
// URL /users/alice -> userId is "alice"
return <h1>Profile for user {userId}</h1>;
}
// Multiple params work the same way:
<Route path="/products/:category/:id" element={<Product />} />
// URL /products/shoes/7 -> { category: "shoes", id: "7" }Now run the actual logic useParams performs: it splits the route pattern and the real URL into pieces and, wherever the pattern has a :name, it grabs the value from the same slot. Fill in the three blanks:
🎯 Your turn: build useParams from scratch
Parse a URL against its pattern to extract { userId, postId }.
// 🎯 YOUR TURN — this is the logic behind useParams().
// A route pattern and a real URL are split into pieces. Where the
// pattern piece starts with ":", the matching URL piece is a PARAM value.
const pattern = "/users/:userId/posts/:postId";
const url = "/users/42/posts/7";
const patternParts = pattern.split("/"); // ["", "users", ":userId", "posts", ":postId"]
const urlParts = url.split("/"); // ["", "users", "42", "posts", "7"]
const params = {};
for (let i = 0; i < patt
...4. Programmatic Navigation
Sometimes you need to change pages from code rather than from a click — for example, redirecting to /dashboard after a successful login. The useNavigate() hook gives you a navigate function: call navigate('/somewhere') and you're there.
// useNavigate() lets you change the URL from CODE (not a click).
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
async function handleSubmit(e) {
e.preventDefault();
await logIn(); // do the work first...
navigate('/dashboard'); // ...then send them there
}
return <form onSubmit={handleSubmit}>{/* ...inputs... */}</form>;
}
// Handy variations:
navigate('/dashboard', { replace: true }); // don't add to history (no "Back" to login)
navigate(-1); // go back one page, like the browser's Back buttonUse { replace: true } after login so the login page isn't left in the history (otherwise pressing Back drops the user right back on it). And navigate(-1) is the same as clicking the browser's Back button.
5. Nested Routes & Outlet
Most apps share a frame across several pages — a dashboard with a fixed sidebar, say. Rather than repeat that sidebar in every component, you nest child routes inside a parent route. The parent renders a shared layout containing an <Outlet />, and React Router drops the matched child page into that slot. The special index route is what shows at the parent's exact path.
// Nested routes share a layout. The parent renders an <Outlet />
// and the matched CHILD route appears in that slot.
import { Outlet, Link } from 'react-router-dom';
// App.jsx — children are nested INSIDE the parent <Route>:
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} /> {/* /dashboard */}
<Route path="settings" element={<Settings />} /> {/* /dashboard/settings */}
<Route path="profile" element={<Profile />} /> {/* /dashboard/profile */}
</Route>
</Routes>
// DashboardLayout.jsx — the shared frame:
function DashboardLayout() {
return (
<div className="dashboard">
<aside>
<Link to="settings">Settings</Link> {/* relative link */}
<Link to="profile">Profile</Link>
</aside>
<main>
<Outlet /> {/* the child route (Settings / Profile / …) renders HERE */}
</main>
</div>
);
}
// The sidebar stays put; only the <Outlet /> content changes between pages.Now /dashboard/settings renders DashboardLayout with Settings inside its <Outlet />. The sidebar never re-mounts; only the outlet's contents change as you click around.
Mini-Challenge: a tiny history stack
No blanks this time — just a brief and an outline. Routers keep a history array so Back works; you'll build a miniature version with push, replace, and pop. Run it and check your output against the expected lines in the comments.
🎯 Mini-Challenge: navigate / replace / goBack
Implement the three functions, then run the sequence.
// 🎯 MINI-CHALLENGE: build a mini "navigate()" history stack
//
// React Router keeps a history of URLs so the Back button works.
// Simulate it with a plain array.
//
// 1. Start with: const history = ["/"];
// 2. Write navigate(url) -> push url onto history, print "Now at: <url>"
// 3. Write navigateReplace(url) -> REPLACE the last entry (no new history),
// then print "Replaced -> <url>"
// 4. Write goBack() -> remove the last entry and pri
...Common Errors (and the fix)
- Page flashes / state resets on every click: you used
<a href="/about">instead of<Link to="/about">. The anchor forces a full server reload; switch to<Link>(or<NavLink>) for internal navigation. useParamsgivesundefined: the destructured key doesn't match the route. Route/users/:userIdmust be read asconst { userId } = useParams()— notidoruser. The names have to be identical.- "useNavigate() may be used only in the context of a Router" / blank screen: you forgot to wrap the app in
<BrowserRouter>. EveryLink,Route, and router hook must live inside that wrapper. - Nested page is blank: the parent layout has no
<Outlet />. Without it, child routes have nowhere to render. - Comparing a param like a number fails: params are always strings, so
userId === 42isfalse. Convert first:Number(userId) === 42.
Pro Tips
- 💡 Put
path="*"last. Routes are read top-to-bottom; a wildcard placed early would swallow everything below it. - 💡 Redirect after login with
{ replace: true }so the user can't press Back into the login form. - 💡 Relative links in nested routes (
<Link to="settings">, no leading slash) resolve against the parent — easier to refactor than absolute paths. - 💡 Params are strings. Wrap with
Number(...)before doing maths or strict comparisons.
📋 Quick Reference
| Goal | Code |
|---|---|
| Turn on routing | <BrowserRouter><App /></BrowserRouter> |
| Define a page | <Route path="/about" element={<About />} /> |
| 404 page | <Route path="*" element={<NotFound />} /> |
| Link (no reload) | <Link to="/about">About</Link> |
| Active-aware link | <NavLink to="/about">…</NavLink> |
| Dynamic segment | path="/users/:userId" |
| Read a param | const { userId } = useParams() |
| Redirect in code | const navigate = useNavigate(); navigate("/x") |
| Go back | navigate(-1) |
| Nested child slot | <Outlet /> (in the parent layout) |
Frequently Asked Questions
Q: Is React Router part of React?
No. React itself has no concept of URLs or pages. React Router is a separate library you add with npm install react-router-dom. It's the de-facto standard for routing in React apps.
Q: When should I use Link vs useNavigate?
Use <Link> for things the user clicks (nav bars, buttons that are really links). Use useNavigate when code decides to move — after a form submits, a login succeeds, or a timer fires.
Q: Why is my useParams value a string when the URL had a number?
A URL is just text, so every param arrives as a string. /users/42 gives you "42". Convert it with Number(userId) if you need to do maths or a strict === comparison.
Q: What's the difference between index and path="" in nested routes?
An index route is what renders at the parent's exact URL (e.g. /dashboard with nothing after it). It's the nested-route way of saying "the default child page."
🎉 Lesson Complete
- ✅
BrowserRouterwraps the app once and turns routing on - ✅
Routes+Routemap URLs to components;path="*"(last) is the 404 - ✅
Linknavigates without a reload;NavLinkstyles the active page - ✅
:paramsegments +useParams()read data out of the URL (always strings) - ✅
useNavigate()redirects from code;{ replace: true }andnavigate(-1)control history - ✅ Nested routes +
Outletshare a layout across pages - ✅ Next lesson: Building a Complete React App — wire all of this into a real project
Sign up for free to track which lessons you've completed and get learning reminders.