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
npm install react-router-domRun 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.
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
- 1Install
react-router-domand wrap the app inBrowserRouterinmain.jsx. - 2Create the page components: Home, Menu, ItemPage, OrderPage and NotFound in
src/pages. - 3Create
Layout.jsxwith a navigation bar and an<Outlet />. - 4Define the routes in
App.jsx, with the layout route wrapping the pages. - 5Replace any plain
<a href>links withLinkorNavLink, then click through every page.
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;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.
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.
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 '/'.
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.
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.
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.
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.
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
- Is the app wrapped in
BrowserRouteronce, above all routes and links? - Did you use
LinkorNavLinkfor internal links (not<a href>)? - Does the layout render
<Outlet />? Without it child routes show nothing. - Child route paths inside a layout are relative (
menu, not/menu); both work, but mixing them confuses beginners. - Did you compare
useParams()strings with strings?Number(id)if your data uses numbers. - 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).
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
- React Router matches the URL to components without reloading the page: wrap the app in
BrowserRouter, defineRoutesandRoute. - Use
LinkandNavLinkfor internal navigation,useNavigatefor navigating from code, andNavigatefor redirects. :paramin a path plususeParams()lets one component serve many URLs; the value is a string.- A layout route renders shared UI and an
<Outlet />for its children;path="*"is the 404 page. - Keep shareable state such as filters in the query string with
useSearchParams. - 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.