Intermediate · Building the frontend · topic 16 of 34 · 30 min
Pages and client-side routing
Explanation
Multi-page apps and single-page apps
In a multi-page app (MPA), every click on a link asks the server for a whole new HTML page, and the browser throws away the old one. In a single-page app (SPA), the browser loads one HTML page and the JavaScript once. After that, when the URL changes, JavaScript draws the new page itself. It uses the browser's History API, so the address bar, the back button and bookmarks still work.
Client-side routing is the part that decides which page component to show for the current URL. A route is one rule: "for this path, show this page".
The Notes app routes
| Path | Page component | Shows |
|---|---|---|
| /login | LoginPage | Email and password form |
| /signup | SignupPage | Name, email and password form |
| /notes | NotesPage | Search bar and the notes list |
| /notes/new | NewNotePage | An empty NoteForm |
| /notes/:id | EditNotePage | NoteForm filled with note :id |
| anything else | NotFoundPage | "Page not found" and a link to /notes |
import { BrowserRouter, Routes, Route, Navigate } from 'react-router';
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Navigate to="/notes" replace />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignupPage />} />
<Route path="/notes" element={<NotesPage />} />
<Route path="/notes/new" element={<NewNotePage />} />
<Route path="/notes/:id" element={<EditNotePage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</BrowserRouter>
);
}A URL parameter is a part of the path that changes, written with a colon in the route: /notes/:id. For /notes/42 the router gives you { id: '42' } from the useParams() hook. It is always a string, so convert it with Number(id) if you need a number. React Router prefers fixed segments, so /notes/new opens NewNotePage and is not read as a note with id "new".
import { Link, useParams } from 'react-router';
function EditNotePage() {
const { id } = useParams(); // /notes/42 gives { id: '42' }
const note = useNote(Number(id)); // loading it comes in the next module
if (!note) return <p>Note not found. <Link to="/notes">Back to notes</Link></p>;
return <NoteForm initial={note} />;
}Use <Link to="/notes/new"> for links inside the app. A plain <a href="/notes/new"> makes the browser download and start the whole app again, losing all state. Link only changes the URL and lets React show the new page. Use a normal <a> for links to other websites.
The URL as state
Some state belongs in the URL, most of all the search text: /notes?q=physics. Then a student can share the link with a friend, a refresh keeps the search, and the back button returns to the previous search. In React Router, useSearchParams() reads and writes the part after the question mark (the query string).
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q') ?? '';
<SearchBar
value={query}
onChange={(q) => setSearchParams(q ? { q } : {}, { replace: true })}
/>Next.js: routes from folders
In Next.js (App Router), you do not write a route list. Each folder inside app/ is a path segment and its page.js file is the page: app/notes/page.js is /notes, app/notes/new/page.js is /notes/new, app/notes/[id]/page.js is /notes/:id (square brackets mark a parameter), and app/not-found.js is the 404 page. Links use the Link component from next/link.
Deep links need the server's help
A deep link is a URL to a page inside the app, like https://notes.example/notes/42, opened directly from a bookmark or a chat message. The browser asks the server for /notes/42. A plain static server has no such file and answers 404. Configure the server to send index.html for every path that is not an API route or a real file; then React Router reads the URL and shows the right page. The Vite dev server already does this; in production you set it up (Next.js handles it for you).
app.use('/api', apiRouter); // real API routes first
app.use('/api', (req, res) => res.status(404).json({
error: { code: 'NOT_FOUND', message: 'No such API route' },
}));
app.use(express.static('dist')); // built JS, CSS and images
app.get('/{*splat}', (req, res) => { // every other path: the SPA
res.sendFile(path.join(import.meta.dirname, 'dist', 'index.html'));
});Watch the lesson
Pages and client-side routing · 1/7
Our Notes app has several pages. In a single-page app, the browser loads once and JavaScript swaps the pages.
Prefer a video by a teacher? Find free videos on “react router v7 tutorial useParams useSearchParams” on YouTube ↗
In real life: Sharing a train search
On a train booking website, after you search for trains from Delhi to Mumbai on a date, the address bar shows something like /trains?from=NDLS&to=BCT&date=2026-11-02. You can send that link to your family on WhatsApp and they see the same list. If you refresh, the search is still there, and the back button returns to your earlier search. Clicking a train opens its details page without reloading the whole site, because the app changes the page on the client.
Practice (0/2 done)
Write matchRoute(pattern, path), the heart of a router. Split both on "/" and ignore empty pieces (so "/notes/" equals "/notes"). If they have a different number of segments, return null. Compare segment by segment: a pattern segment starting with ":" is a parameter and matches anything (store it in the result, decoded with decodeURIComponent); any other segment must be equal, or return null. Return the params object ({} when there are none).
Keep the search text in the URL. Write two functions using URL and URLSearchParams. readSearch(url) returns the value of the q parameter, or "" if there is none. withQuery(url, q) returns the full URL as a string with q set to the trimmed q, keeping the other parameters; if the trimmed q is empty, it removes q instead. Example: withQuery("https://notes.example/notes?page=2", "exam dates") gives "https://notes.example/notes?page=2&q=exam+dates".
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.