Skip to content

Intermediate · Building the frontend · topic 16 of 34 · 30 min

Pages and client-side routing

Explanation

A single-page app (SPA) loads once and then swaps pages in JavaScript when the URL changes, without a full reload. A router such as React Router (or the folders of a Next.js app) maps each URL, like /notes/42, to a page component. Put shareable state such as the search text in the URL, use Link for in-app links, and make the server send index.html for deep links.

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.

Think of it like this: A multi-page app is like walking out of one shop and into the next building for every item. A single-page app is like a food court in a mall: you stay inside one building and just move between counters. The sign above each counter (the URL) still tells you exactly where you are, so you can tell a friend "meet me at counter 12".

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

PathPage componentShows
/loginLoginPageEmail and password form
/signupSignupPageName, email and password form
/notesNotesPageSearch bar and the notes list
/notes/newNewNotePageAn empty NoteForm
/notes/:idEditNotePageNoteForm filled with note :id
anything elseNotFoundPage"Page not found" and a link to /notes
javascript
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>
  );
}
React Router v7 (imported from "react-router"). path="*" catches every unknown URL: the 404 page.

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".

javascript
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} />;
}
Read the id from the URL, and handle a note that does not exist.

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).

javascript
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q') ?? '';

<SearchBar
  value={query}
  onChange={(q) => setSearchParams(q ? { q } : {}, { replace: true })}
/>
The URL is now the single source of truth for the search text.
Tip: Pass { replace: true } while the user is typing. Otherwise every key press adds a new history entry, and the back button walks through "p", "ph", "phy" one letter at a time.

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).

javascript
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'));
});
SPA fallback in Express 5. Unknown API paths still get a JSON 404, not the HTML page.
Careful: Hiding a route in the frontend (for example sending logged-out users from /notes to /login) is only for comfort. Anyone can call the API directly, so the server must check the session on every request. You will build both sides in the authentication module.

Watch the lesson

Pages and client-side routing · 1/7

SPA: load once, swap pages in JavaScript

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)

Task 1 · Code · Javascript

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).

Task 2 · Code · Javascript

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

Question 1

1.What makes a single-page app different from a multi-page app?

Question 2

2.Inside the Notes app, how should you link to /notes/new?

Question 3

3.A student wants to share their search for "physics" with a friend. Where should the search text live?

Question 4

4.The app works when you click around, but opening https://notes.example/notes/42 from a bookmark shows the server's 404 page. What is the fix?

Question 5

5.For the route /notes/:id and the URL /notes/42, what does useParams() return?

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.

← Previous: State: what the screen remembers