0 / 91
Week 9 · Day 62 of 91

Routing and page structure

React Frontend Fundamentals

Objective

Map URLs to pages and preserve understandable navigation.

A React component is a reusable functional block with inputs (props), internal state, and rendered output.

  • client-side routing
  • route parameters
  • not-found states

Why this matters

Your app is one page that changes. That is fine until someone wants to bookmark an equipment record, send a colleague a link to it, or press the back button. Right now none of those work, because every screen has the same URL. Today you give each screen an address.

By the end of the hour, typing /equipment/PUMP-3 into the address bar opens that record directly, back and forward behave, and a made-up URL shows a page that helps rather than a blank screen.

Client-side routing

On Day 9 you linked pages together with <a href="about.html">. Clicking one makes the browser throw away the current document, request a new one, and rebuild everything. In a React app that also destroys all your state and re-runs every effect.

Client-side routing keeps a single HTML document and swaps components instead. A router does three jobs:

  1. it reads the current URL path,
  2. it renders the component you mapped to that path,
  3. it intercepts internal link clicks and pushes the new URL into browser history without a page load.

Point three is what keeps the URL honest. The address bar, the back button, and bookmarks all keep working — the browser's history is updated, just not by requesting a new document.

Install the standard library for this:

npm install react-router-dom

An address decoder

A memory bus puts an address on the lines and a decoder enables exactly one device to respond. Routing is the same decode step: the URL path is the address, the route table is the decoder, and one component gets enabled to drive the output. /equipment/PUMP-3 selects one device and hands it the low-order bits — the id — as its input.

Setting up the route table

The router needs to wrap your whole app so every component inside can see the current URL. Do that once, in src/main.tsx:

import { BrowserRouter } from "react-router-dom";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>,
);

Then declare the mapping in App.tsx. <Routes> picks the single best match among its <Route> children and renders that route's element:

import { Routes, Route, NavLink, Outlet } from "react-router-dom";

function Layout() {
  return (
    <>
      <header>
        <h1>Equipment Maintenance</h1>
        <nav aria-label="Main">
          <NavLink to="/">Dashboard</NavLink>
          <NavLink to="/equipment">Equipment</NavLink>
          <NavLink to="/login">Log in</NavLink>
        </nav>
      </header>
      <main>
        <Outlet />
      </main>
    </>
  );
}

export default function App() {
  return (
    <Routes>
      <Route element={<Layout />}>
        <Route index element={<DashboardPage />} />
        <Route path="equipment" element={<EquipmentListPage />} />
        <Route path="equipment/:id" element={<EquipmentDetailPage />} />
        <Route path="login" element={<LoginPage />} />
        <Route path="*" element={<NotFoundPage />} />
      </Route>
    </Routes>
  );
}

The outer <Route> has no path, only an element. It is a layout route: it always renders, and <Outlet /> marks the hole where the matched child page goes. That is how the header and nav stay on screen while the page below them changes — written once, not copied into four files.

index means "the parent's path with nothing after it", so that route handles /.

Link and NavLink instead of <a>. Both render a real <a> element in the DOM — so right-click, open-in-new-tab, and screen readers all behave normally — but they intercept the plain click and route without reloading. NavLink adds one thing: when its to matches the current URL it sets aria-current="page" on the link, which announces "current page" to assistive technology and gives you a hook for styling.

A plain `<a href="/equipment">` still reloads the page

It works, in that the right screen appears — but the whole app restarts, state is lost, and every effect runs again. It usually feels like a slow flicker. Use <a> only for links leaving your site.

Route parameters

Most URLs identify a resource: not "the equipment page" but this specific machine. A segment starting with a colon is a route parameter — a wildcard that matches any value and hands it to you by name.

path="equipment/:id"     matches  /equipment/PUMP-3     id = "PUMP-3"
                         matches  /equipment/MTR-1      id = "MTR-1"

Read it inside the component with useParams:

import { useParams, Link } from "react-router-dom";

export function EquipmentDetailPage() {
  const { id } = useParams();
  const item = equipment.find((entry) => entry.id === id);
  // ...
}

useParams returns an object whose keys are your parameter names. The values are typed string | undefined, because TypeScript cannot know which route rendered this component — so you have to handle the missing case, which is exactly the case you should be handling anyway.

This mirrors the API you built on Day 41: GET /api/equipment/:id. Same shape, same reasoning. Keep URLs resource-oriented and predictable — plural collection, then id — and a colleague can guess them.

A room number, not a floor plan

"Building 2, room 314" identifies one room; you can write it on a note and hand it to anyone. A URL without a parameter is like saying "the room I was looking at" — meaningless to the next person. Route parameters are what make a screen shareable.

Not-found states

There are two different ways a URL can fail, and they need two different answers.

The path matches nothing. /equpiment is a typo, and no route covers it. That is what path="*" catches — a catch-all that must be listed so it only wins when nothing else does. Give it a page with a heading, a short explanation, and a link back to somewhere real. Never a blank screen.

The path matches but the resource does not exist. /equipment/NOPE-9 matches equipment/:id perfectly; there is just no such machine. The route renders, find returns undefined, and the component must say so:

if (!item) {
  return (
    <section>
      <h2>Equipment not found</h2>
      <p>No equipment matches the id "{id}".</p>
      <Link to="/equipment">Back to all equipment</Link>
    </section>
  );
}

That if (!item) is not defensive padding. Once the data comes from a server in Week 10, deleted records will produce exactly this, from links that were valid yesterday.

Direct URLs break on a real server unless you configure it

Vite's dev server sends index.html for any unknown path, so /equipment/PUMP-3 works while you develop. A plain static host does not: it looks for a file at that path and returns 404. Every host has a setting for this, usually called a history or SPA fallback, and it is the number one "works locally, 404s in production" bug for single-page apps. Week 11 configures it properly.

Walkthrough

Create src/pages/ with one file per page, then wire the table above.

mkdir -p src/pages

Each page component is ordinary React — no new concepts, just a screen:

export function EquipmentListPage() {
  return (
    <section>
      <h2>All equipment</h2>
      <ul>
        {equipment.map((item) => (
          <li key={item.id}>
            <Link to={`/equipment/${item.id}`}>{item.name}</Link>
          </li>
        ))}
      </ul>
    </section>
  );
}

Start the dev server and click a name. The URL changes to /equipment/PUMP-3, the header stays put, and only the region inside <Outlet /> changes. Press back: you return to the list. Now copy the detail URL, open a new tab, and paste it — the record loads directly, because the route table reads the address rather than remembering a click.

Checkpoint

With the detail page open, look at the DevTools Network tab and press back. No document request appears. Say why: the router changed history and re-rendered, it did not ask the server for a new page.

Pair mode — before you write the route table

Today's mode is pair: the AI proposes, you verify. Before accepting any generated change, inspect the diff, run your checks, and be able to explain the behaviour of every route you keep.

"Propose a route tree from these user journeys. Keep URLs resource-oriented and predictable."

List your journeys first — "see today's status", "look up one machine", "log in". Then judge the proposal against them. If a route does not serve a journey, cut it.

Your turn

Deliverable: direct URLs work and unknown routes show a useful page.

  1. Install react-router-dom and wrap <App /> in <BrowserRouter> in main.tsx.
  2. Create four pages in src/pages/: DashboardPage, EquipmentListPage, EquipmentDetailPage, LoginPage. Each returns a <section> with its own <h2>.
  3. Build the route table with the pathless layout route, index, equipment, equipment/:id, login, and *.
  4. Put the header and a <nav aria-label="Main"> of NavLinks in the layout, with <Outlet /> below in a <main> element.
  5. In EquipmentListPage, link each item to /equipment/${item.id} with Link.
  6. In EquipmentDetailPage, read id with useParams, look the item up, and render the not-found block when it is missing.
  7. Create NotFoundPage with a heading and a link home, and confirm /nope shows it.
  8. Test four URLs by typing them in the address bar: /, /equipment, /equipment/PUMP-3, and /equipment/NOPE-9. The last two must give different pages.
  9. Keyboard pass: Tab to a nav link and press Enter. Confirm the active link carries aria-current="page" in the DevTools element inspector.

You are done when

Four URLs work when pasted fresh into the address bar, back and forward move between them, and both kinds of not-found produce a page with a way out.

Common pitfalls

  • Forgetting <BrowserRouter>. Any router hook then throws, with a message about needing to be used within a <Router>. Wrap the app once, at the root.
  • Putting path="*" first. <Routes> picks the best match rather than the first, so this is less fatal than it looks — but keep the catch-all last so the table reads in the order a person would.
  • Using <a href> for internal links. The screen is right and the app restarted. Watch for the flash and the lost state.
  • Assuming useParams gives you a valid id. It gives you whatever was in the URL. Always handle "no such resource".

Verify it yourself

Open today's reference, React's Learn section, and find where it discusses routing and what React itself does not provide.

  1. React points to routers rather than shipping one. Find that statement and note which libraries it names. Why does routing sit outside React's own scope?
  2. In the React Router documentation, find useNavigate. Write one sentence on when you would navigate from code instead of using a Link — you will need it on Day 66 after a login submit.

Record both in notes/day-62.md. Knowing which problems React deliberately leaves to other libraries is part of knowing what React is.

The hour

  1. 0–5 min Recall

    Without notes, state yesterday’s main idea and one unresolved question.

  2. 5–20 min Learn

    Read only the listed concept notes and official reference sections needed today.

  3. 20–48 min Build

    Create dashboard, equipment list, equipment detail, and login routes.

  4. 48–55 min Explain and verify

    Run the result, inspect evidence, and explain the data/control flow in your own words.

  5. 55–60 min Quiz and commit

    Complete the quiz, record one lesson, and commit the verified change when applicable.

What to hand in

Deliverable

Direct URLs work and unknown routes show a useful page.

Working with AI today

AI as pair programmer

Define one small task, review the plan, inspect the diff, run checks, and explain every changed section.

Propose a route tree from these user journeys. Keep URLs resource-oriented and predictable.

References

End-of-day quiz

Q1 What does a route parameter commonly identify?
Q2 Which result best proves today’s work is complete?
Q3 Before accepting an AI-generated code change, what should you do?

Explain-back gate

Pass the quiz above to unlock completion.

Quiz + explain-back checks required.