Routing with React Router
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.
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:
npm install react-routerReact 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:
// 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>
);// 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 inLayoutstays on screen while pages change below it.indexmarks the child shown at the parent's own URL (/), andpath="*"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; itsclassNameandstyleprops accept a function that receives{ isActive }. Theendprop stops "Home" from matching every URL.:numberis 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:
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 withNumber()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.htmlfor unknown paths.
Key points
- Client-side routing changes the URL and the screen without reloading the page.
- Install
react-router(version 8);react-router-domis no longer used. BrowserRouter,RoutesandRoutemap URLs to components; nested routes render in an<Outlet />.LinkandNavLinknavigate;useNavigatenavigates from code;useParamsreads:params.- Data mode (
createBrowserRouter,loader,useLoaderData) loads data before a page renders. - Configure your host to serve
index.htmlfor 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
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?
Version 8 removed react-router-dom; everything comes from react-router (DOM pieces from react-router/dom).
2. Why use <Link to="/cart"> instead of <a href="/cart">?
A plain anchor triggers a full page load; Link lets the router switch screens in the browser.
3. What does <Outlet /> do in a layout route?
Nested routes render where the parent puts its <Outlet />.
4. For the route trains/:number, what does useParams() return at /trains/JP101?
Parameters come back as an object of strings named after the :param in the path.
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
