Skip to content
Topic 12 of 13
Theory

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>; NavLink knows 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 Route with <Outlet />.

Two pages

Easy+10 XP

Write the App component (the router around it already exists). In it:

  • a <nav> with two Links: Home (address /) and About (address /about);
  • routes: / shows <h1>Home</h1>, /about — <h1>About</h1>, any other address — <h1>Not found</h1>.
/           → Home
click About → About
/oops       → Not found

Connect the connector to solve tasks

Task code runs on your own computer — faster and more reliable than in the browser. This needs the free CodeWithMe connector app. You only connect it once.

Looking for the connector on this computer…

  1. 1
    Download the connector for your system (Windows, macOS or Linux).Download the connectorOn the download page pick the file with your system in its name, e.g. windows-amd64.
  2. 2Run it and choose a folder for your solutions. It will install Python and Node.js there by itself when needed.
  3. 3
    Enter below the 6-digit code shown in the connector window (“Code for CodeWithMe”). If the browser asks about local network access — allow it.

The connector is open source (on GitHub), works only on your computer and runs code only after you enter the pairing code.