Routes: React Router
Many pages in one application
A real application has several pages: the home page, a catalog, a product card, a cart, a profile. You could keep "the
current page" in state and show the right component with if. But then:
- the address in the browser doesn't change — you can't bookmark the page or send it to a friend;
- the "Back" and "Forward" buttons don't work;
- after reloading the page the user always ends up on the home page.
A React application is a single-page application (SPA): the browser loads one HTML file, and after that JavaScript redraws the page itself. For addresses to work with this, you need routing: address → which component to show. The most popular library for this is React Router.
How it works: on a link click React Router does not reload the page. It changes the address through the browser's
History API (history.pushState) and re-renders the right components. It's fast, like an app, and still has real addresses.
Setting it up
In a project the library is installed with npm install react-router-dom, and you import what you need:
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
On our site it's already available as the global object ReactRouterDOM:
const { BrowserRouter, Routes, Route, Link } = ReactRouterDOM;
The whole application is wrapped in a router — usually in main.jsx:
ReactDOM.createRoot(document.getElementById("root")).render(
<BrowserRouter>
<App />
</BrowserRouter>,
);
BrowserRouter works with the browser's real address. In the tasks on the site the code runs in a sandbox that has no
address bar, so MemoryRouter is used there — it keeps the address in memory. Everything else works the same.
Routes and Route
function App() {
return (
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/catalog" element={<CatalogPage />} />
<Route path="/cart" element={<CartPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
);
}
<Routes>looks at the current address and picks one matching route.<Route path="…" element={…}>— "if the address is this, show this element".path="*"matches any address — that's the 404 page for everything that didn't match.
Everything outside <Routes> (the header, the menu, the footer) is shown on all pages.
Links: Link and NavLink
An ordinary <a href="/cart"> reloads the whole page and resets the application state. Inside the application you use Link:
<nav>
<Link to="/">Home</Link>
<Link to="/catalog">Catalog</Link>
<NavLink to="/cart" className={({ isActive }) => (isActive ? "active" : "")}>
Cart
</NavLink>
</nav>
Link draws an ordinary <a> but intercepts the click. NavLink is the same, plus it knows whether the link is active:
handy for highlighting the current menu item.
URL parameters: useParams
Product cards have addresses like /products/1, /products/2… You're not going to write a route for every product. The
changing part of the address is marked with a colon:
<Route path="/products/:id" element={<ProductPage />} />
const { useParams } = ReactRouterDOM;
function ProductPage() {
const { id } = useParams(); // for /products/42 → "42"
const product = products.find((p) => p.id === Number(id));
if (!product) return <p>Product not found</p>;
return <h1>{product.title}</h1>;
}
Parameters are always strings: "42", not 42. When comparing with a numeric id, convert with Number. Usually data
is loaded by the parameter: useFetch(/api/products/${id}) from the previous topic.
Parameters after ? (/catalog?sort=price&page=2) are read with the useSearchParams hook:
const [searchParams, setSearchParams] = useSearchParams();
const sort = searchParams.get("sort"); // "price"
setSearchParams({ sort: "name" }); // the address becomes ?sort=name
Navigating from code: useNavigate
Sometimes you need to navigate not on a link click but after an action: a form was submitted — open the order page; the user logged out — go to the home page:
const { useNavigate } = ReactRouterDOM;
function OrderForm() {
const navigate = useNavigate();
async function handleSubmit(e) {
e.preventDefault();
const order = await sendOrder();
navigate(`/orders/${order.id}`);
}
return <form onSubmit={handleSubmit}>…</form>;
}
navigate("/path")— navigate (like a link click, with a history entry);navigate("/path", { replace: true })— replace the current entry: "Back" won't return here;navigate(-1)— back, like the browser button.
To navigate right during rendering (for example, "no access — go to the login page") there's the
<Navigate to="/login" replace /> component.
Nested routes and Outlet
Most pages share a layout: a header, a menu, a footer. It's described by a parent route, and the place for child pages
is marked with <Outlet />:
const { Outlet } = ReactRouterDOM;
function Layout() {
return (
<div>
<header>
<Link to="/">Shop</Link> <Link to="/cart">Cart</Link>
</header>
<main>
<Outlet /> {/* the current page goes here */}
</main>
<footer>© 2026</footer>
</div>
);
}
function App() {
return (
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<HomePage />} />
<Route path="/cart" element={<CartPage />} />
<Route path="/products/:id" element={<ProductPage />} />
</Route>
<Route path="/login" element={<LoginPage />} /> {/* without the shared layout */}
</Routes>
);
}
The nesting of routes mirrors the nesting of the interface: Layout stays in place (and keeps its state) when navigating
between pages inside it.
Protected pages
A profile or orders page must not be shown without logging in. A common technique is to check and redirect:
function ProfilePage() {
const { user } = useAuth(); // a custom hook over the context from the useContext topic
if (!user) return <Navigate to="/login" replace />;
return <h1>Hello, {user.name}</h1>;
}
It's important to understand: this protects the interface, not the data. The real access check is always done by the server — the user can change code in the browser.
Deploying an SPA
When the application is put on hosting, the address /products/42 is, for the server, a path to a file that doesn't exist.
So the hosting is configured to serve index.html for any address, and then React Router picks the route. Netlify, Vercel
and Cloudflare Pages have a simple setting for this (or it's on by default for SPAs).
Summary
- An SPA: one HTML page, navigation without reloading; React Router connects the address with components.
<BrowserRouter>around the application;<Routes>+<Route path element>;path="*"— 404.<Link to>instead of<a href>;NavLinkknows whether the link is active./products/:id+useParams()— URL parameters (strings!);useSearchParams— after?.useNavigate()— navigating from code;<Navigate>— a redirect during rendering.- A shared layout — a parent
Routewith<Outlet />.