React Router Setup

React by itself has no concept of pages or URLs — every app you’ve built so far renders one component tree at one URL. React Router is the standard library that adds client-side routing: it lets you map different URL paths to different components, so your app can feel like a multi-page site while staying a single-page application (SPA) that never triggers a full browser reload. This lesson covers installing React Router, wiring up the core components, and rendering your first routes.

Overview: How React Router Works

A traditional multi-page website asks the server for a new HTML document every time the URL changes. A React SPA instead loads one HTML page and one JavaScript bundle, and then manages everything else — including the URL — on the client. React Router listens for changes to the browser’s URL (via the History API) and, instead of letting the browser fetch a new page, it swaps out which React components are rendered to match the new path. The address bar updates, the back/forward buttons work, and bookmarked URLs still work — but no network round trip happens for the page itself.

React Router v6+ (the current major version) is built around three ideas:

  • A router component (most commonly BrowserRouter) that uses the HTML5 History API to keep the UI in sync with the URL. It must wrap any part of your app that uses routing features.
  • Routes and Route, which declare which component should render for which path. Routes looks at the current URL and renders the single best-matching Route‘s element.
  • Link and NavLink, which render anchor tags that change the URL through React Router (updating state and calling history.pushState under the hood) instead of asking the browser to navigate away and reload.

Because routing state lives in the URL, and React re-renders whenever the matched route changes, navigating your app is just another form of the same render cycle you already know: URL changes → Routes re-evaluates which Route matches → React renders the new element → React reconciles the tree and commits only the DOM differences.

Installing React Router

React Router is a separate package, not part of core React. Install it with npm or yarn in your project directory:

npm install react-router-dom

You import routing components from react-router-dom (the DOM-flavored package, as opposed to react-router-native for React Native).

Syntax

The minimal setup has three layers: a router at the root, a Routes block that lists possible paths, and Route elements that pair a path with an element to render.

import { BrowserRouter, Routes, Route } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  );
}
Part Purpose
BrowserRouter Provides routing context to the whole tree; keeps the UI synced with the URL via the History API. Render it once, near the root.
Routes A container that inspects the current URL and renders exactly one matching child Route‘s element.
Route Declares a path to match and the element to render when it matches.
path A URL pattern, e.g. /, /about, or /users/:id (dynamic segments use a leading colon).
element A JSX element (not a bare component reference) to render for that route, e.g. element={<About />}.
Link Renders an <a> that navigates via React Router instead of a full page load. Takes a to prop instead of href.

Examples

Example 1: A basic two-page app

This is the smallest complete setup: an entry file that mounts the app, and an App component that defines two routes and a nav bar to move between them.

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <App />
  </StrictMode>
);
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";

function Home() {
  return <h1>Home Page</h1>;
}

function About() {
  return <h1>About Page</h1>;
}

function NotFound() {
  return <h1>404: Page Not Found</h1>;
}

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Visiting / renders “Home Page”, visiting /about renders “About Page”, and visiting anything else (like /xyz) matches the catch-all path="*" route and renders “404: Page Not Found”. Clicking the Link elements changes the URL and swaps the rendered page without a full browser reload.

Example 2: Highlighting the active link with NavLink

NavLink works like Link but knows when its to path matches the current URL, letting you style the active link differently.

import { NavLink } from "react-router-dom";

function NavBar() {
  return (
    <nav>
      <NavLink
        to="/"
        className={({ isActive }) => (isActive ? "link active" : "link")}
      >
        Home
      </NavLink>
      <NavLink
        to="/about"
        className={({ isActive }) => (isActive ? "link active" : "link")}
      >
        About
      </NavLink>
    </nav>
  );
}

export default NavBar;

Renders two navigation links. Whichever one matches the current URL receives the "active" class in addition to "link", so you can style it (e.g. underline or bold it) in your CSS. NavLink‘s className prop accepts a function that receives { isActive }, computed automatically by comparing the link’s to path against the current location.

Example 3: A shared layout with Outlet

Real apps usually want a persistent layout (header, footer) around every page. Nesting a parent Route with child routes and an Outlet placeholder achieves this without repeating the layout in every page component.

import { BrowserRouter, Routes, Route, Outlet, Link } from "react-router-dom";

function Layout() {
  return (
    <div>
      <header>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
      </header>
      <main>
        <Outlet />
      </main>
      <footer>© 2026 My Site</footer>
    </div>
  );
}

function Home() {
  return <p>Welcome home!</p>;
}

function About() {
  return <p>About us.</p>;
}

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

export default App;

The header and footer render for every page because Layout is the parent route’s element. Outlet is where React Router injects whichever child route matched — Home for the index route (the default child at /) or About for /about. This nested pattern is covered in depth in the “Nested Routes” lesson; here it’s shown to demonstrate that setup scales beyond flat route lists.

Under the Hood: What Happens on Navigation

Understanding the sequence helps you reason about routing bugs:

  • On initial load, BrowserRouter reads window.location once and stores the current path in its internal state, then provides it via context to every descendant.
  • On mount, Routes reads that context, compares the current path against each child Route‘s path, and renders the single best match’s element.
  • On a Link click, React Router calls event.preventDefault() on the anchor’s click, then calls history.pushState to update the URL without a network request, and updates its internal location state.
  • On that state update, BrowserRouter re-renders, the new location flows down through context, Routes re-evaluates the match, and React renders (and reconciles/commits) whatever new element is now the best match — unmounting the old page’s component and mounting the new one.
  • On browser back/forward, the same thing happens, but triggered by the browser’s popstate event instead of a Link click.

Common Mistakes

Mistake 1: Using Routes without a router

Forgetting to wrap Routes in a BrowserRouter throws an error because Routes needs routing context to know the current location.

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
    </Routes>
  );
}

This throws: “useLocation() may be used only in the context of a <Router> component.” Fix it by wrapping the routes in BrowserRouter (usually once, near the top of the app):

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
      </Routes>
    </BrowserRouter>
  );
}

Mistake 2: Using a plain <a> tag for internal navigation

A regular anchor tag forces a full page reload, throwing away all React state and re-downloading the entire JS bundle — defeating the purpose of an SPA.

function NavBar() {
  return <a href="/about">About</a>;
}

Use Link (or NavLink) instead, which intercepts the click and navigates client-side:

import { Link } from "react-router-dom";

function NavBar() {
  return <Link to="/about">About</Link>;
}

Mistake 3: Putting the catch-all route first

Since React Router v6 picks the best match rather than the first match, order matters less than in v5 — but a wildcard listed before specific routes can still be confusing to read and is best kept last by convention, as shown in Example 1’s path="*" route appearing after the named routes.

Best Practices

  • Install and import from react-router-dom (not react-router) for web apps.
  • Render BrowserRouter once, as close to the root of your app as practical (often directly inside App or around it in your entry file).
  • Always use Link/NavLink for internal navigation, never a plain <a href>, so the SPA doesn’t reload.
  • Pass a JSX element to element (e.g. element={<About />}), not a bare component reference (element={About}), so React Router can render it with any props you supply.
  • Add a catch-all <Route path="*" element={<NotFound />} /> as the last route so unmatched URLs show a friendly 404 instead of a blank page.
  • Use NavLink instead of Link whenever you need to style the currently active navigation item.
  • Keep route definitions in one place (often App.jsx) so the app’s URL structure is easy to see at a glance.

Practice Exercises

  • Exercise 1: Create a new React app, install react-router-dom, and set up three routes: /, /contact, and a catch-all 404 route. Add a nav bar with Links to the first two.
  • Exercise 2: Convert your nav bar to use NavLink and add CSS so the active link appears bold or underlined.
  • Exercise 3: Add a shared Layout component with a header and footer, and nest your Home and Contact routes inside it using Outlet, so the header/footer appear on every page without being repeated in each page component.

Summary

  • React Router adds client-side routing to React, mapping URLs to components without full page reloads.
  • Install it with npm install react-router-dom and import from that package.
  • BrowserRouter must wrap any part of the app using routing features; it syncs the UI with the browser’s URL via the History API.
  • Routes renders the single best-matching child Route‘s element for the current URL.
  • Use Link and NavLink instead of plain anchor tags for internal navigation so the app stays a true SPA.
  • Outlet lets nested routes share a parent layout, rendering the matched child route in place.
  • A catch-all path="*" route at the end of your route list handles unmatched URLs gracefully.