React Router
Reviewed & published by Brayan K
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.
Part of the free React course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
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
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>
<App />
</BrowserRouter>
);
// App.jsx — map URL paths to components with <Routes> and <Route>.
import { Routes, Route } from 'react-router-dom';
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<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:
// === 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 route of routes) {
if (route.path === "*" || route.path === url) {
return route.page;
}
}
}
console.log(matchRoute("/")); // Home
console.log(matchRoute("/about")); // About
console.log(matchRoute("/contact")); // Contact
console.log(matchRoute("/banana")); // NotFound (fell through to "*")
// 👀 Notice "/banana" skips every exact path and lands on the wildcard —
// that is exactly how the 404 page works.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 — 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;
}
}
}
console.log(matchRoute("/")); // 👉 should print: Home
console.log(matchRoute("/shop")); // 👉 should print: Shop
console.log(matchRoute("/xyz")); // 👉 should print: NotFound
// ✅ Expected output:
// Home
// Shop
// NotFound2. 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 to="/">Home</Link>
<Link to="/about">About</Link>
<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 — 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 < patternParts.length; i++) {
const piece = patternParts[i];
// 1) A param piece starts with ":" — detect it.
if (piece.startsWith(___)) { // 👉 the single character that marks a param
// 2) The KEY is the piece without the leading ":". Use slice(1).
const key = piece.___(1); // 👉 method that drops the first character
// 3) The VALUE is the matching piece from the real url at the same index.
params[key] = urlParts[___]; // 👉 which index? same position as the pattern piece
}
}
console.log(params); // 👉 should be: { userId: "42", postId: "7" }
// ✅ Expected output:
// { userId: '42', postId: '7' }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}> </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 />} />
<Route path="settings" element={<Settings />} />
<Route path="profile" element={<Profile />} />
</Route>
</Routes>
// DashboardLayout.jsx — the shared frame:
function DashboardLayout() {
return (
<div className="dashboard">
<aside>
<Link to="settings">Settings</Link>
<Link to="profile">Profile</Link>
</aside>
<main>
<Outlet />
</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: 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 print
// "Back to: <new last entry>"
//
// Then run this sequence:
// navigate("/about")
// navigate("/contact")
// goBack()
// navigateReplace("/login")
//
// ✅ Expected output:
// Now at: /about
// Now at: /contact
// Back to: /about
// Replaced -> /login
// your code hereCommon 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.
- useParams gives undefined: the destructured key doesn't match the route. Route /users/:userId must be read as const { userId } = useParams() — not id or user. 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>. Every Link, 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 === 42 is false. 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
- ✅ BrowserRouter wraps the app once and turns routing on
- ✅ Routes + Route map URLs to components; path="*" (last) is the 404
- ✅ Link navigates without a reload; NavLink styles the active page
- ✅ :param segments + useParams() read data out of the URL (always strings)
- ✅ useNavigate() redirects from code; { replace: true } and navigate(-1) control history
- ✅ Nested routes + Outlet share a layout across pages
Practice quiz
Where must you place <BrowserRouter> in a React Router v6 app?
- Around the whole app once, so every Route and router hook lives inside it
- Around each individual <Route> separately
- Only inside the component that calls useNavigate
- It is optional and only needed for dynamic routes
Answer: Around the whole app once, so every Route and router hook lives inside it. BrowserRouter turns on client-side routing for everything nested inside it, so you wrap the entire app exactly once (usually in main.jsx).
Which route definition acts as the catch-all 404 page?
- <Route path="/404" .../>
- <Route path="*" .../> placed last
- <Route path="" .../>
- <Route index .../>
Answer: <Route path="*" .../> placed last. path="*" matches any URL no other route caught. Because routes are read top to bottom, it must come last or it would swallow everything below it.
Why should you use <Link> instead of a plain <a href> for internal navigation?
- <a> tags are not valid inside React components
- <Link> automatically adds query parameters
- <a href> triggers a full page reload that tears down the app and loses state, while <Link> swaps the page in JavaScript
- There is no difference; they behave identically
Answer: <a href> triggers a full page reload that tears down the app and loses state, while <Link> swaps the page in JavaScript. A plain anchor makes the browser refetch and re-run the whole app, losing React state. Link intercepts the click and updates the URL with the History API, keeping state.
Given <Route path="/users/:userId" ...>, how do you read userId inside the component?
- const userId = useParams
- const { userId } = useParams()
- const userId = props.userId
- const { userId } = useNavigate()
Answer: const { userId } = useParams(). useParams() returns an object whose keys match the dynamic segments. The destructured key must exactly match the name after the colon.
A URL param from useParams() for /users/42 comes back as what type?
- A number (42)
- A string ("42")
- An object { value: 42 }
- undefined until parsed
Answer: A string ("42"). URLs are just text, so every param arrives as a string. Use Number(userId) before doing maths or a strict === comparison.
Which hook lets you change the URL from code, e.g. after a login succeeds?
- useParams()
- useNavigate()
- useLocation()
- useRoutes()
Answer: useNavigate(). useNavigate() returns a navigate function: navigate('/dashboard') redirects programmatically rather than from a click.
What does navigate('/dashboard', { replace: true }) do?
- Opens the route in a new tab
- Navigates without adding a new entry to the history stack, so Back won't return to the current page
- Replaces the entire route configuration
- Reloads the page from the server
Answer: Navigates without adding a new entry to the history stack, so Back won't return to the current page. replace: true swaps the current history entry instead of pushing a new one — ideal after login so Back doesn't return to the login form.
In nested routes, where does the matched child route render?
- Wherever you place an <Outlet /> in the parent layout
- Automatically at the bottom of the page
- Inside a <Routes> tag in the child
- It replaces the parent component entirely
Answer: Wherever you place an <Outlet /> in the parent layout. The parent layout renders an <Outlet />, and React Router drops the matched child component into that slot while the surrounding layout stays mounted.
What is the purpose of an index route inside a parent route?
- It renders at the parent's exact path as the default child
- It sets the order routes are evaluated
- It marks the 404 fallback
- It enables lazy loading
Answer: It renders at the parent's exact path as the default child. An index route is what shows at the parent's exact URL (e.g. /dashboard with nothing after it) — the default child page.
What does <NavLink> add over a regular <Link>?
- It performs a full page reload
- It knows whether it points at the current page, so you can style the active link
- It only works for external URLs
- It caches the destination component
Answer: It knows whether it points at the current page, so you can style the active link. NavLink is a Link that's aware of the active route. Its className function receives { isActive } so you can highlight the current page.
Continue this course
- Previous: Error Boundaries
- Next: TypeScript with React — Type your props, state, events, and hooks with TypeScript
- Quick reference: React cheat sheet