Web Development with React
Hands-onIntermediate 35 min· Building real apps · 12 of 21

Routing with React Router

Turn a single-page React app into a multi-page experience with React Router: routes, links, a shared layout, URL parameters, redirects, a 404 page and navigation from code. You will build the Brew & Bean site with Home, Menu, Item and Order pages.

In this tutorial you will give the Brew & Bean website several pages: Home (/), Menu (/menu), one item (/menu/espresso), Order (/order) and a not-found page. A React app is really one HTML page; a router watches the address bar, decides which component to show, and updates the URL without reloading the page. This is called a single-page application (SPA).

Prerequisites: a Vite + React project, props, state, and the public/menu.json file from *useEffect and fetching data*. We use React Router in its classic declarative style, with plain JavaScript (JSX). React Router also has newer data APIs and a framework mode; they build on the same ideas, so check its official docs for them later.

Install and set up

Terminal
npm install react-router-dom

Run this inside your project folder. In version 7 the same components are also available from the react-router package; react-router-dom keeps working and re-exports them.

Wrap your whole app in BrowserRouter once, in main.jsx. It gives every component below it access to the current URL.

src/main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App.jsx';

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

Routes and links

A Route pairs a URL path with an element to show. Routes picks the best matching route. Use Link instead of a plain <a href>: a normal anchor makes the browser reload the whole page and throws away all React state, while Link changes the URL and swaps components instantly.

Build the pages in this order

  1. 1Install react-router-dom and wrap the app in BrowserRouter in main.jsx.
  2. 2Create the page components: Home, Menu, ItemPage, OrderPage and NotFound in src/pages.
  3. 3Create Layout.jsx with a navigation bar and an <Outlet />.
  4. 4Define the routes in App.jsx, with the layout route wrapping the pages.
  5. 5Replace any plain <a href> links with Link or NavLink, then click through every page.
src/pages/Home.jsx
import { Link } from 'react-router-dom';

function Home() {
  return (
    <section>
      <h1>Brew & Bean</h1>
      <p>Fresh coffee in Indiranagar, Bengaluru. Open 8 am to 10 pm. Pickup only.</p>
      <Link to="/menu">See the menu</Link>
    </section>
  );
}

export default Home;
src/pages/Menu.jsx
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';

function Menu() {
  const [menu, setMenu] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch('/menu.json')
      .then((response) => response.json())
      .then((data) => setMenu(data))
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <p>Loading menu...</p>;

  return (
    <section>
      <h1>Menu</h1>
      <ul>
        {menu.map((item) => (
          <li key={item.id}>
            <Link to={'/menu/' + item.id}>{item.name}</Link> - ₹{item.price}
          </li>
        ))}
      </ul>
    </section>
  );
}

export default Menu;

Each item links to /menu/espresso, /menu/cold-brew and so on. A shorter fetch version is used to keep the focus on routing; add error handling as in the previous lesson for real work.

URL parameters

You do not want to write a separate component for each of the eight items. Put a colon in the path to make a parameter: /menu/:itemId. useParams() returns an object with the value from the current URL. The value is always a string.

src/pages/ItemPage.jsx
import { useEffect, useState } from 'react';
import { Link, useParams } from 'react-router-dom';

function ItemPage() {
  const { itemId } = useParams();
  const [menu, setMenu] = useState(null);

  useEffect(() => {
    fetch('/menu.json')
      .then((response) => response.json())
      .then((data) => setMenu(data));
  }, []);

  if (menu === null) return <p>Loading...</p>;

  const item = menu.find((m) => m.id === itemId);
  if (!item) {
    return (
      <section>
        <h1>We could not find that item</h1>
        <Link to="/menu">Back to the menu</Link>
      </section>
    );
  }

  return (
    <section>
      <h1>{item.name}</h1>
      <p>Category: {item.category}</p>
      <p>Price: ₹{item.price}</p>
      <Link to="/menu">Back to the menu</Link>
    </section>
  );
}

export default ItemPage;

A valid path with a bad parameter (/menu/pizza) still matches the route, so the component itself must handle 'not found'.

A shared layout with Outlet

Header and footer should stay while the page content changes. Make a layout route: a Route with an element and child routes nested inside. The layout renders <Outlet /> where the matching child should appear. NavLink is a Link that knows whether it is the current page, so you can highlight it.

src/Layout.jsx
import { NavLink, Outlet } from 'react-router-dom';

function navStyle({ isActive }) {
  return { fontWeight: isActive ? 'bold' : 'normal', marginRight: '1rem' };
}

function Layout() {
  return (
    <>
      <header>
        <nav>
          <NavLink to="/" end style={navStyle}>Home</NavLink>
          <NavLink to="/menu" style={navStyle}>Menu</NavLink>
          <NavLink to="/order" style={navStyle}>Order</NavLink>
        </nav>
      </header>
      <main>
        <Outlet />
      </main>
      <footer>Brew & Bean, Indiranagar, Bengaluru</footer>
    </>
  );
}

export default Layout;

end on the Home link stops it from counting as active on every page, because every path starts with '/'.

src/App.jsx
import { Navigate, Route, Routes } from 'react-router-dom';
import Layout from './Layout';
import Home from './pages/Home';
import Menu from './pages/Menu';
import ItemPage from './pages/ItemPage';
import OrderPage from './pages/OrderPage';
import NotFound from './pages/NotFound';

function App() {
  return (
    <Routes>
      <Route element={<Layout />}>
        <Route index element={<Home />} />
        <Route path="menu" element={<Menu />} />
        <Route path="menu/:itemId" element={<ItemPage />} />
        <Route path="order" element={<OrderPage />} />
        <Route path="coffee" element={<Navigate to="/menu" replace />} />
        <Route path="*" element={<NotFound />} />
      </Route>
    </Routes>
  );
}

export default App;

index is the default child at '/'. Navigate redirects old URLs. The path '*' matches anything else, so it is your 404 page.

src/pages/NotFound.jsx
import { Link } from 'react-router-dom';

function NotFound() {
  return (
    <section>
      <h1>Page not found</h1>
      <p>That address does not exist.</p>
      <Link to="/">Go to the home page</Link>
    </section>
  );
}

export default NotFound;

Navigating from code

Sometimes you navigate because something happened, such as an order being placed, and not because the user clicked a link. useNavigate() returns a function for that. Pass { replace: true } so the form page is replaced in the history and the Back button does not return to a finished form.

src/pages/OrderPage.jsx
import { useNavigate } from 'react-router-dom';

function OrderPage() {
  const navigate = useNavigate();

  function handleSubmit(event) {
    event.preventDefault();
    // In a real app: POST /orders first, then use the id it returns.
    const newOrderId = 'demo-123';
    navigate('/menu', { replace: true, state: { justOrdered: newOrderId } });
  }

  return (
    <section>
      <h1>Place an order</h1>
      <form onSubmit={handleSubmit}>
        <button type="submit">Place demo order</button>
      </form>
    </section>
  );
}

export default OrderPage;

The optional state object travels with the navigation, and the target page can read it with useLocation().state. For anything the user should be able to bookmark, put it in the URL instead.

Query strings

Filters and search terms belong in the query string (/menu?category=tea) so users can share and bookmark them. useSearchParams works like useState, but the state lives in the URL.

Using useSearchParams in Menu.jsx
import { useSearchParams } from 'react-router-dom';

// inside Menu():
const [searchParams, setSearchParams] = useSearchParams();
const category = searchParams.get('category') || 'all';

const visible =
  category === 'all' ? menu : menu.filter((item) => item.category === category);

// a button that changes the URL to /menu?category=tea
// <button onClick={() => setSearchParams({ category: 'tea' })}>Tea</button>

Reloading the page or sending the link to a friend keeps the filter. Add these lines to the Menu component from earlier and render visible instead of menu.

What you should see
Open http://localhost:5173/ and click through: the header and footer stay still while the content changes. /menu/espresso shows the Espresso page. /menu/pizza shows 'We could not find that item'. /coffee redirects to /menu. /anything-else shows 'Page not found'. The page never fully reloads between clicks.

Refresh gives 404 after deployment

Your SPA has one real HTML file. If a user opens /menu/espresso directly or refreshes there, the web server looks for a file at that path and may return 404. The dev server handles this for you. On a host you must add a fallback rule that serves index.html for unknown paths (static hosts such as Netlify and Vercel have settings for this). See *Deploy and share your app*.

Common mistakes

If routing does not work

  1. Is the app wrapped in BrowserRouter once, above all routes and links?
  2. Did you use Link or NavLink for internal links (not <a href>)?
  3. Does the layout render <Outlet />? Without it child routes show nothing.
  4. Child route paths inside a layout are relative (menu, not /menu); both work, but mixing them confuses beginners.
  5. Did you compare useParams() strings with strings? Number(id) if your data uses numbers.
  6. Does the * route exist so wrong URLs show a useful page?

How this comes up in interviews: "What is client-side routing and how is it different from server-side routing?" (the browser swaps components using JavaScript instead of requesting a new page from the server) and "How would you protect a route?" (render a wrapper route that checks login state and returns <Navigate to="/login" replace /> when the user is not signed in; remember the server must still protect the data).

Prompt · Ask AI to design your routes
I am building a React app with React Router (declarative mode, plain JavaScript). The app is: [describe your app and its pages].

List the routes (path and component name), say which should share a layout, which need URL parameters or query strings, and what the 404 behaviour should be. Then write the Routes block for App.jsx. Do not add features I did not ask for.

What to expect: Check that dynamic parts use :params, shared UI uses a layout route with Outlet, and there is a catch-all route.

Try it: Grow the site

1. Add an /about page with opening hours and a link in the navigation. 2. On the item page, add a Previous and Next link to the neighbouring items in the menu. 3. Create /orders/:orderId that shows an order's status and add a link from a confirmation message. 4. Add a RequireStaff wrapper route that redirects to /login unless a staff flag in state is true, and put a /kitchen page behind it.

Key takeaways

  1. React Router matches the URL to components without reloading the page: wrap the app in BrowserRouter, define Routes and Route.
  2. Use Link and NavLink for internal navigation, useNavigate for navigating from code, and Navigate for redirects.
  3. :param in a path plus useParams() lets one component serve many URLs; the value is a string.
  4. A layout route renders shared UI and an <Outlet /> for its children; path="*" is the 404 page.
  5. Keep shareable state such as filters in the query string with useSearchParams.
  6. A deployed SPA needs a fallback to index.html, or refreshing a deep link gives a 404.

Check yourself

Finishing the quiz marks the lesson as done.

1. Why use <Link to="/menu"> instead of <a href="/menu"> inside a React Router app?

2. A route is path="menu/:itemId" and the URL is /menu/cold-brew. What does useParams() return?

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

4. Which route catches every URL that nothing else matched?

5. Where should a shareable filter such as 'tea only' live?

6. After deploying, opening /menu/espresso directly gives a 404 from the host, though it works while clicking links. Why?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free