Routing with React Router

React · Lesson 23 of 29 6 min read Updated

A real app has many screens: home, products, a product's details, the cart. Each should have its own URL, so users can bookmark it, share it and use the browser's Back button. Changing screens without reloading the whole page is called client-side routing. React does not include a router, and the most widely used one is React Router, now at version 8.

In this lesson you will see how client-side routing works under the hood, then set up React Router with routes, links, URL parameters, nested layouts, programmatic navigation and data loaders.

How client-side routing works

In a traditional website, every link asks the server for a new HTML page. In a single-page application (SPA), the server sends one index.html and your JavaScript bundle. When the user clicks a link, the router changes the URL with the browser's History API, without a reload, and React renders the component that matches the new URL.

The Try-it editor cannot load React Router, so here is a tiny hand-made router that uses the part of the URL after # to show the idea. Real apps should use a proper router.

React JSXTry it yourself
import { useSyncExternalStore } from "react";
import { createRoot } from "react-dom/client";

function subscribe(callback) {
  window.addEventListener("hashchange", callback);
  return () => window.removeEventListener("hashchange", callback);
}
function useHashPath() {
  return useSyncExternalStore(subscribe, () => window.location.hash.slice(1) || "/");
}

const trains = { "JP101": "Pink City Express", "JP102": "Aravalli Rajdhani" };

function Home() {
  return <p>Welcome to Rail Info. Pick a train from the menu.</p>;
}
function TrainList() {
  return (
    <ul>
      {Object.entries(trains).map(([no, name]) => (
        <li key={no}><a href={`#/trains/${no}`}>{name}</a></li>
      ))}
    </ul>
  );
}
function TrainDetails({ number }) {
  return trains[number] ? <p>Train {number}: {trains[number]}</p> : <p>No train with number {number}.</p>;
}

function App() {
  const path = useHashPath();
  const match = path.match(/^\/trains\/(\w+)$/); // a route with a parameter

  let page;
  if (path === "/") page = <Home />;
  else if (path === "/trains") page = <TrainList />;
  else if (match) page = <TrainDetails number={match[1]} />;
  else page = <p>Page not found: {path}</p>;

  return (
    <>
      <nav>
        <a href="#/">Home</a> | <a href="#/trains">Trains</a> | <a href="#/fares">Fares</a>
      </nav>
      <p><small>Current path: {path}</small></p>
      {page}
    </>
  );
}

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

Click through the menu and a train: the URL changes, the right component appears, and the page never reloads. A real router does the same thing, but adds clean URLs without #, nested layouts, data loading, scroll handling and much more.

Setting up React Router

Install the package in your Vite project:

Terminal
npm install react-router

React Router can be used in three modes. Declarative mode is the simplest and is shown first. Data mode adds loaders and actions. Framework mode turns React Router into a full framework with server rendering, similar to Next.js.

Wrap your app in BrowserRouter in main.jsx, then describe the pages with Routes and Route:

React JSX
// src/main.jsx
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
import App from "./App.jsx";

createRoot(document.getElementById("root")).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);
React JSX
// src/App.jsx
import { Routes, Route, Link, NavLink, Outlet, useParams, useNavigate } from "react-router";

function Layout() {
  return (
    <>
      <nav>
        <NavLink to="/" end>Home</NavLink> | <NavLink to="/trains">Trains</NavLink>
      </nav>
      <main>
        <Outlet /> {/* the matching child route renders here */}
      </main>
    </>
  );
}

function TrainDetails() {
  const { number } = useParams(); // from the URL /trains/:number
  const navigate = useNavigate();
  return (
    <>
      <h2>Train {number}</h2>
      <button onClick={() => navigate("/trains")}>Back to all trains</button>
    </>
  );
}

export default function App() {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<p>Welcome to Rail Info</p>} />
        <Route path="trains" element={<TrainList />} />
        <Route path="trains/:number" element={<TrainDetails />} />
        <Route path="*" element={<p>Page not found</p>} />
      </Route>
    </Routes>
  );
}

function TrainList() {
  return <Link to="/trains/JP101">Pink City Express</Link>;
}

Here is what each piece does:

  • <Route path="..." element={...}> maps a URL pattern to a component. Routes can be nested: child paths are relative to their parent.
  • <Outlet /> marks where a parent layout shows the matching child. The navigation bar in Layout stays on screen while pages change below it.
  • index marks the child shown at the parent's own URL (/), and path="*" catches every unknown URL, which gives you a 404 page.
  • <Link to="..."> renders an <a> that navigates without reloading. Never use a plain <a href> for internal pages, or the whole app reloads and loses its state.
  • <NavLink> is a link that knows whether it is active, so you can highlight the current page; its className and style props accept a function that receives { isActive }. The end prop stops "Home" from matching every URL.
  • :number is a URL parameter. useParams() returns { number: "JP101" }. Parameters are always strings.
  • useNavigate() returns a function for navigating from code, for example after a form is saved.

For query strings such as /trains?class=3A, use useSearchParams(), which works like useState for the part after ?.

Data mode: loaders

In data mode you define routes as objects with createBrowserRouter. Each route can have a loader that fetches data while the router navigates, before the page renders, which avoids the request waterfalls from Data Fetching Patterns:

React JSX
import { createBrowserRouter, useLoaderData } from "react-router";
import { RouterProvider } from "react-router/dom";
import { createRoot } from "react-dom/client";

async function trainLoader({ params }) {
  const res = await fetch(`/api/trains/${params.number}`);
  if (!res.ok) throw new Response("Not found", { status: 404 });
  return res.json();
}

function TrainPage() {
  const train = useLoaderData(); // whatever the loader returned
  return <h2>{train.name} departs at {train.departs}</h2>;
}

const router = createBrowserRouter([
  { path: "/", element: <Home /> },
  { path: "/trains/:number", loader: trainLoader, element: <TrainPage /> },
]);

createRoot(document.getElementById("root")).render(<RouterProvider router={router} />);

Create the router once, outside any component. Routes in data mode can also have action functions for form submissions and an errorElement for failures.

Deploying an SPA

With BrowserRouter, a user may open https://example.com/trains/JP101 directly. The server must answer such URLs with your index.html, so the router can take over in the browser. Most static hosts have a "single-page app" or "rewrite all to index.html" setting. Without it, refreshing any page except the home page shows the server's 404 error.

Common mistakes

  • Using <a href> for internal links. It reloads the whole app. Use <Link> or <NavLink>.
  • Forgetting <Outlet />. Child routes of a layout render nowhere and the page looks empty.
  • Treating params as numbers. useParams() gives strings; convert with Number() before maths or strict comparisons.
  • Mixing versions. Copying v5 code (Switch, useHistory) into a v8 app fails. Check which version a tutorial uses.
  • No server fallback. Configure your host to serve index.html for unknown paths.

Key points

  • Client-side routing changes the URL and the screen without reloading the page.
  • Install react-router (version 8); react-router-dom is no longer used.
  • BrowserRouter, Routes and Route map URLs to components; nested routes render in an <Outlet />.
  • Link and NavLink navigate; useNavigate navigates from code; useParams reads :params.
  • Data mode (createBrowserRouter, loader, useLoaderData) loads data before a page renders.
  • Configure your host to serve index.html for every route of an SPA.

Exercise

Plan the routes for a small college website using React Router: a layout with a navigation bar, a home page at /, a courses list at /courses, a course page at /courses/:slug that shows the slug from the URL, a contact page and a 404 page. Write App.jsx with nested routes and a Course component that reads the slug with useParams.

Show solution
React JSX
import { Routes, Route, NavLink, Outlet, Link, useParams } from "react-router";

function Layout() {
  return (
    <>
      <nav>
        <NavLink to="/" end>Home</NavLink> | <NavLink to="/courses">Courses</NavLink> |{" "}
        <NavLink to="/contact">Contact</NavLink>
      </nav>
      <Outlet />
    </>
  );
}

function Courses() {
  return (
    <ul>
      <li><Link to="/courses/bca">BCA</Link></li>
      <li><Link to="/courses/bcom">B.Com</Link></li>
    </ul>
  );
}

function Course() {
  const { slug } = useParams();
  return <h2>Course: {slug.toUpperCase()}</h2>;
}

export default function App() {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<h1>Government College, Jaipur</h1>} />
        <Route path="courses" element={<Courses />} />
        <Route path="courses/:slug" element={<Course />} />
        <Route path="contact" element={<p>Call 0141 000 0000</p>} />
        <Route path="*" element={<p>Page not found</p>} />
      </Route>
    </Routes>
  );
}

All pages are children of Layout, so the navigation stays visible. main.jsx must wrap <App /> in <BrowserRouter> for these routes to work.

Quick Quiz

1. Which package do you install for React Router 8?

2. Why use <Link to="/cart"> instead of <a href="/cart">?

3. What does <Outlet /> do in a layout route?

4. For the route trains/:number, what does useParams() return at /trains/JP101?

Learn React with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call