Skip to content

Routing

A React app renders one component tree into one HTML document. Real sites have a home page, an about page, a details page, each with its own URL. Routing is how those two facts meet: the URL decides which components render, and clicking around the app updates the URL. React Router is the library most React apps use for this, and the course dedicates a whole section to it, building a van rental app called VanLife along the way. This chapter covers the core pieces; nested routes and route params build on them.

Client-side routing: MPA vs SPA

In a traditional multi-page application (MPA), every navigation is a round trip. The browser requests /about from the server, the server assembles an HTML page and sends it back, and the browser throws away the current page to load the new one. That full-page swap is visible as a telltale flicker on every click.

A single-page application (SPA) loads one document once. The name is a little misleading: the site can still have many pages in the user's eyes. What is single is the document. After the first load, the React app itself decides what to show for each URL. Navigating to /about swaps components inside the running app, with no new document requested and no flicker. If a view needs fresh data, the app fetches JSON in the background and updates in place, the pattern covered in Fetching data.

Client-side routing is the mechanism that makes this work: a library watches the URL, intercepts navigation, and renders the matching components instead of letting the browser reload. There is a real prize for keeping the document alive. A full page load wipes all React state; staying inside one document means your state survives every navigation.

Version note

The course teaches React Router 6. Version 7 merged React Router with Remix and consolidated the packages, so react-router is the current import path. react-router-dom survives in v7 as a re-export and is the package v6 and the course use, which is why every example in this handbook imports from it; version 8 dropped it, so a fresh install imports these components from react-router.

React Router documents three modes: declarative, the BrowserRouter and Route components this chapter covers; data, which pairs createBrowserRouter with loaders and actions; and framework, a Vite plugin that adds typed route modules, code splitting, and server rendering. The component and hook names in this chapter are unchanged across those versions.

BrowserRouter, Routes, and Route

Three components define a basic setup: BrowserRouter wraps the app and enables routing, Routes holds your route definitions, and each Route maps one path to one element.

jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom'

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

Under the hood, BrowserRouter is a context provider. Wrapping the app with it gives every component below access to the router's tools, which is why it sits at the top. Some codebases rename it on import (import { BrowserRouter as Router }) since the full name is a mouthful.

Each Route takes two props. path is the part of the URL after the domain: / for the home page, /about for the about page. element is the JSX to render when the URL matches that path, written as an actual element, <Home /> rather than Home. When the URL is /about, React Router renders <About /> where the Routes component sits; everything outside Routes renders on every page.

Visit a path with no matching route and nothing renders at all. That gap gets fixed by the catch-all route below.

The HTML way to move between pages is an anchor tag, and it is exactly wrong for a SPA: clicking an <a href="/about"> triggers a full page load, which discards the running app and every piece of state in it. React Router's answer is the Link component.

jsx
import { Link } from 'react-router-dom'

function Header() {
  return (
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
    </nav>
  )
}

Link takes a to prop instead of href, and it reads like English: a link to the following route. In the browser it still renders a real anchor tag, so screen readers, right-click menus, and your CSS element selectors all see an ordinary <a>. The difference is the click handler: React Router intercepts the click, updates the URL, and swaps the rendered components, all without reloading the document. A counter sitting at ten stays at ten as you move between pages. Inside a React Router app, internal navigation always goes through Link; plain anchors are for external URLs.

Navigation bars usually highlight the page you are currently on. NavLink exists for exactly this: it behaves like Link, except its className and style props can take a function instead of a plain value. React Router calls that function with an object containing an isActive boolean, true when the link's route matches the current URL. This is the render props pattern, covered in Render props, applied to a prop other than children.

jsx
import { NavLink } from 'react-router-dom'

function Header() {
  return (
    <nav>
      <NavLink
        to="/about"
        className={({ isActive }) => isActive ? 'active-link' : ''}
      >
        About
      </NavLink>
    </nav>
  )
}

Whatever the function returns becomes the class name, so the active-link class applies only while /about is the current route, and your CSS handles the rest. The style prop works the same way with an inline style object:

jsx
const activeStyles = { fontWeight: 'bold', textDecoration: 'underline' }

<NavLink
  to="/about"
  style={({ isActive }) => isActive ? activeStyles : null}
>
  About
</NavLink>

Class name or inline style is a team convention call; both are fully supported, so follow whatever the project already does.

A catch-all 404 route

Users land on paths that don't exist: an outdated link, a typo, someone else's broken reference. Without a fallback, React Router renders nothing there. The fix is a catch-all route, sometimes called a splat route, whose path is a single asterisk.

jsx
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  <Route path="*" element={<NotFound />} />
</Routes>

path="*" matches anything no other route claims, much like a universal selector in CSS. A typical NotFound component pairs a short message with a Link back to the home page. Order in the list doesn't matter: React Router 6 scores every route against the URL and picks the best match, so the catch-all only wins when nothing more specific does. Putting it last is convention and readability rather than necessity.

The mechanism behind all of this is the browser's History API. Link calls event.preventDefault() on a plain left click, pushes the new URL with history.pushState, and lets the router's context provider notify subscribers that the location changed. Matching routes re-render; the document never reloads. That is also the whole story of why state survives: the JavaScript environment is never torn down.

Route matching in v6 is rank-based. Each path segment earns a score, with static segments beating dynamic ones and dynamic ones beating the splat, so definition order carries no meaning. This replaced v5's first-match-wins model, where an unfenced / at the top of the list shadowed everything below it.

NavLink counts ancestors as active

The router considers every ancestor of the current URL matched, so a link to /host reports isActive while you are at /host/income. Usually that is what you want in a nav bar. When it isn't, the end prop tells that link to match only its exact path. This matters most with nested routes, where several routes render at once by design.

Client-side routing also has a deployment catch. BrowserRouter produces clean URLs like /about, but the server never heard of that path; only the router knows it. A user who refreshes or deep-links there hits the server directly, so the host must be configured to serve index.html for every route and let React Router take over from the URL. Most static hosts have a one-line rewrite rule for exactly this.

Server setup is also where v7's framework mode changes the picture. Declarative and data mode leave the build and the server to you, while framework mode takes both over with a Vite plugin that handles server rendering. Everything in this chapter sits on the declarative side of that line.

JunoThe URL picks the components A single-page app loads one document and then swaps what you see as you click around, so there is no flicker and nothing gets thrown away.

Wrap your app in BrowserRouter, list your pages as Route components inside Routes, and each path gets its own element to render.

Use Link instead of an anchor tag so clicking keeps the app running, and add a path="*" route so visitors who mistype a URL see a friendly page instead of a blank screen.

JunoThe URL picks the components The working set is small: BrowserRouter at the top, Routes holding Route elements that map path to element, Link to for navigation, and NavLink when the nav bar should highlight the current page.

Pass className or style a function and read isActive from the object it receives.

Finish the setup with a path="*" catch-all for a 404 page; route scoring means its position in the list is irrelevant.

JunoThe URL picks the componentsLink prevents the default click and drives history.pushState, and BrowserRouter is a context provider broadcasting location changes, which is why state survives navigation.

Matching is rank-based rather than order-based, ancestors count as active for NavLink until you add end, and clean URLs require the server to rewrite every path to the app shell.

The declarative-mode APIs here carry straight into v7, which also documents data mode for loaders and actions and framework mode for full-stack integration.

Next up: Nested routes and layouts, where routes gain shared chrome and routes inside routes.