Command Palette

Search for a command to run...

Hectal
PHASE 13Intermediate ~30 min· topic 2 of 6

Topic 13.2

React Router Basics

In one line

React Router maps URLs to components: <Routes> picks the best <Route> for the current URL, <Link> changes the URL without reloading, and <NavLink> knows when it points at the current page; apps wrap everything in <BrowserRouter>, while tests and this preview use <MemoryRouter>.

Think of it like this

A hotel receptionist with a list of room numbers. A guest says a room number (the URL), the receptionist looks at the list (the routes), and sends the guest to the right room (the component). If nobody has that number, the guest is sent to the "sorry, no such room" desk (a not-found route). The receptionist always looks for the best match on the whole list, not the first line that roughly fits.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

React Router
The most used routing library for React. Version 8 is imported from the single package react-router.
BrowserRouter
The router for real apps. It reads and writes the browser's address bar with the History API.
MemoryRouter
A router that keeps its history in memory. You give it a start URL with initialEntries.
Routes
The component that compares the current URL with its <Route> children and renders the best match.
Route
One rule: a path pattern and the element to show when it matches.
Link
A router-aware <a>: changes the URL without reloading the page.
NavLink
A Link that knows when it points at the current page and marks itself active with aria-current="page".
Active link
A navigation link whose target matches the current URL, usually styled to show "you are here".
Library mode
How you use React Router: declarative (components), data (route objects with loaders) or framework (Vite plugin, files, server rendering).

Step by step

01Install and wrap the app

In a real Vite app, install the package and put <BrowserRouter> around <App /> once, in the file that mounts React. Every component inside can now use links, routes and router hooks.

src/main.jsxwhole filejsx
// npm i react-router
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
import App from "./App.jsx";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>,
);

02Declare the routes

In App, list the pages. Anything outside <Routes> (the header, the nav) shows on every page; only the matching route's element changes.

src/App.jsxwhole filejsx
import { Routes, Route, NavLink } from "react-router";
import Home from "./pages/Home.jsx";
import About from "./pages/About.jsx";

export default function App() {
  return (
    <>
      <nav>
        <NavLink to="/">Home</NavLink>
        <NavLink to="/about">About</NavLink>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </>
  );
}

03How a click becomes a new screen

Link cancels the browser's navigation, the router pushes the new location, and every component that reads the location (here <Routes> and each NavLink) re-renders. The header and nav components stay mounted, so their state survives.

How a click becomes a new screendiagram
Rendering diagram…

04In the preview: MemoryRouter

The live examples swap <BrowserRouter> for <MemoryRouter initialEntries={["/"]}> and show useLocation().pathname in a small address bar. Everything else is identical, which is the point: your routes don't know which router they live in.

preview.jsxwhole filejsx
<MemoryRouter initialEntries={["/blog/hello"]}>
  <App />
</MemoryRouter>

05Style the active link

Use the active class or a className function. Prefer styling [aria-current="page"] in CSS: the same attribute that helps screen readers then drives the visual style, so the two can't disagree.

nav.csswhole filecss
nav a { color: #495057; text-decoration: none; }
nav a[aria-current="page"] { color: #1971c2; font-weight: 700; border-bottom: 2px solid currentColor; }

06Know the three modes

The same Link, NavLink, Outlet and hooks work in all modes. What changes is where routes are defined and who loads data.

Know the three modesdiagram
Rendering diagram…

Try it yourself

  1. 1

    Add a Contact page

    In the first example, add a Contact component, a <Route path="/contact"> and a <Link to="/contact">. Change initialEntries to ["/contact"]. Predict what the page shows on load before you run it.

  2. 2

    Remove end

    In the NavLink example, remove end from "Blog home". Predict its class and aria-current on /blog/hello, then run. Then change initialEntries to ["/"]: which links are active now?

  3. 3

    Test the ranking

    In the ranking example, add "/teams/42/edit" to urls. Which route matches? (Hint: :teamId matches exactly one segment.) Add a route that handles it.

Code & diagrams

Routes, Link, and why a plain <a> is wrong New tab

The plain `<a>` is ignored by the router. In a real browser it would reload the whole page from the server; this preview only logs `(link to /about)`. `<Link>` changes the route in place.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

plain <a> -> / Home
<Link> -> /about About us
NavLink: active class, aria-current and end New tab

On `/blog/hello`: Home is not active (`/` is special), "Blog home" isn't either because of `end`, "Blog section" is (it matches the start of the path), and the function `className` replaced the default `active` class.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Home: class= aria-current=null
Blog home: class= aria-current=null
Blog section: class=active aria-current=page
Hello post: class=tab tab-on aria-current=page
Route ranking: the best match wins, not the first New tab

Four separate memory routers, one per URL. Even with `*` written first, `/teams/new` picks the static route and `/teams/42` the dynamic one. `*` only wins when nothing else matches.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

/teams matched All teams
/teams/new matched New team form (static)
/teams/42 matched One team (:teamId)
/players matched Not found (*)
Upgrading imports from v6/v7 to v8tsx
// v6 (two packages)
import { BrowserRouter, Link } from "react-router-dom";

// v7 (react-router-dom still works, but only re-exports react-router)
// v8: react-router-dom is gone. One package:
import { BrowserRouter, Routes, Route, Link, NavLink } from "react-router";

// Data mode in the DOM: RouterProvider also exists in "react-router/dom"
// (that version wires in react-dom's flushSync for you)
import { RouterProvider } from "react-router/dom";

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use a hook outside the router

Move <AddressBar /> out of <MemoryRouter> in the first example (render it above the router).

terminal
$ Console
── what you'll see ──
Uncaught Error: useLocation() may be used only in the context of a <Router> component.
An error occurred in the <AddressBar> component.
 
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://react.dev/link/error-boundaries to learn more about error boundaries.

Break #2

Import from react-router-dom in v8

Copy a v6 tutorial: import { BrowserRouter } from "react-router-dom"; in a project that has only react-router 8 installed.

terminal
$ npm run dev
── what you'll see ──
[vite] Internal server error: Failed to resolve import "react-router-dom" from "src/main.jsx". Does the file exist?

Myth vs fact

Myth

Routes are matched top to bottom, so order matters.

Fact

React Router ranks all routes and picks the most specific match. Static segments beat :params, which beat *. Order only matters for readability.

Myth

Link is a button that calls navigate.

Fact

Link renders a real <a href>, so it works with the keyboard, screen readers, "copy link" and "open in new tab". Use buttons for actions, links for navigation.

Myth

You need react-router-dom for web apps.

Fact

Since v7 the DOM parts live in react-router, and v8 removed react-router-dom entirely.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Version 7 removed the v7_startTransition future flag: router state updates are wrapped in React.startTransition by default. <BrowserRouter useTransitions={false}> turns that off. This matters when a route suspends (Topic 14.3): the old screen stays visible instead of a fallback flashing.

  • ▸

    Link only intercepts "plain" left clicks: it lets the browser handle Ctrl/Cmd/Shift/Alt clicks, middle clicks and links with target="_blank" or reloadDocument, so native behaviours keep working. If you build your own link with useLinkClickHandler, you get the same rules.

  • ▸

    <Routes> can take a location prop to render routes for a different location than the URL, which is how "modal routes" keep the previous page rendered behind a dialog (pass state: { backgroundLocation } in the link).

  • ▸

    With basename="/app" on the router, every to and every matched path are relative to /app, so the same routes work when the SPA is deployed under a sub-path.

Remember this

  1. 1

    React Router is a library, installed with npm i react-router. Version 8 is current (8.4 in this course). Everything is imported from one package, "react-router". In version 6 the browser parts lived in a second package, react-router-dom; version 7 merged them into react-router and kept react-router-dom only as a re-export for old code; version 8 removed react-router-dom completely. If you see import { Link } from "react-router-dom" in a tutorial, it is v6/v7 code: change the package name. Version 8 also needs React 19.2.7 or newer and ships as ES modules only.

  2. 2

    A router component owns the current location and gives it to everything inside. <BrowserRouter> uses the real address bar through the History API (Topic 13.1): you put it once around your app in main.jsx. <MemoryRouter> keeps the history in an array in memory; you pass the starting URL with initialEntries={["/about"]}. It is used in tests (Phase 17), in React Native-like environments, and in the live examples here, because the preview frame can't change its own URL. <HashRouter> stores the path after a # and is only for hosts where you can't add an SPA fallback. All three give the same hooks and components to your code, so the routes you write work under any of them.

  3. 3

    **<Routes> and <Route>.** Inside the router, <Routes> holds a list of <Route path="..." element={<Page />} />. On every render, <Routes> looks at the current URL, ranks every route and renders the element of the best match only. Ranking means static segments (/teams/new) beat dynamic ones (/teams/:teamId, Topic 13.3), which beat the catch-all *; the order you write them in doesn't matter. If nothing matches, <Routes> renders nothing at all, which is why you add a path="*" route for a not-found page (Topic 13.4). Paths are matched without regard to letter case unless you add caseSensitive.

  4. 4

    **<Link to="/about">** renders a real <a href="/about">, so it is a proper link for screen readers, keyboard users, right-click "copy link" and Ctrl/Cmd+click "open in new tab". On a normal click it calls preventDefault() and tells the router to navigate, which updates the URL and re-renders <Routes>, keeping the rest of the page and its state. A plain <a href> inside a React Router app does a full page load (or, in this preview, nothing). Useful props: replace (replace the current history entry instead of adding one), state (an invisible object attached to the navigation, read with useLocation().state), reloadDocument (force a real page load), and preventScrollReset (data routers only, Topic 13.6).

  5. 5

    **<NavLink>** is a Link that knows whether it is active, meaning its to matches the current URL. By default it adds class="active" and aria-current="page" when active, so you can style it with CSS (nav a.active { ... }) and screen readers announce "current page". className, style and children can be functions that receive { isActive, isPending, isTransitioning } (isPending works with data routers, Topic 13.5). A NavLink to /blog is also active on /blog/hello, because it matches the start of the path; add the **end** prop to make it active only on exactly /blog. The link to / is special: it is only active on / itself, otherwise it would be active everywhere.

  6. 6

    Library modes. React Router can be used in three ways, each adding features on top of the last. Declarative mode: <BrowserRouter> + <Routes>/<Route> + Link/NavLink + hooks (this topic and 13.3-13.4); you fetch data yourself. Data mode: routes are defined as objects outside React with createBrowserRouter and rendered with <RouterProvider>, and each route can have a loader, an action and an error boundary (Topic 13.5). Framework mode: React Router's Vite plugin adds file-based route modules, server rendering, code splitting and type-safe routes (the successor of the Remix framework, briefly in Topic 13.5). Pick declarative for small apps or when another library owns your data, data mode for loaders and actions in an SPA, framework mode for a full app.

Explain it without notes

01

What does <BrowserRouter> do, and why do tests and this preview use <MemoryRouter>?

02

How does <Routes> decide which <Route> to render?

03

What is the difference between Link, NavLink and a plain <a> in a React Router app?

04

Describe React Router's three modes and when you'd pick each.

Practice

01

Build a three-page site (Home, Menu, Contact) with a NavLink nav where the current page is bold, inside a MemoryRouter that starts on /menu.

02

Make a NavLink whose text shows "(you are here)" when active, using the children function.

03

Rewrite this v6 code for React Router 8: import { BrowserRouter, Routes, Route, Link } from "react-router-dom";

Trade-offs

  • ↔

    Declarative mode is the simplest to learn and keeps routes next to your JSX, but each page fetches its own data after it renders, which can create loading waterfalls. Data mode fixes that at the cost of defining routes outside React.

  • ↔

    NavLink without end highlights a whole section (good for top-level menus), with end only the exact page (good for tabs). Choose per menu, not globally.

Done when you can

  • I can set up React Router 8 with BrowserRouter and import everything from react-router.

  • I can declare pages with Routes and Route and explain how the best match is chosen.

  • I use Link for navigation and know why a plain <a> reloads the page.

  • I can style the active NavLink and explain end and aria-current.

  • I can name the three library modes and what each adds.

  • I can use MemoryRouter with initialEntries for tests and previews.