Routing & the History API

Advanced · 20 min read · ▶ live playground · ✦ checkpoint

State made your page internally consistent. Routing makes a page's current view show up in the address bar. When the URL says "settings" and the UI renders settings, the user can bookmark it, share it, refresh it, and use Back and Forward without losing where they were.

This is URL-driven UI: the URL is part of the state that decides what view appears. The page still uses the state-render loop from the last lesson, but now one piece of state comes from location.

Routes are view names in the URL

A route is a URL pattern that maps to a view. In a server-rendered site, /pricing might ask the server for the pricing page. In a single-page application, one HTML page stays loaded while JavaScript swaps the visible view.

The important part is not the tool. It is the contract:

  • /dashboard means show the dashboard.
  • /lessons means show lessons.
  • /settings means show settings.

The URL becomes a source of truth for navigation state. Other facts can stay in memory - which task checkbox is focused, whether a dropdown is open, or a temporary draft the user has not saved. Use the URL for meaningful location, not every tiny UI detail.

In a normal page, URL-driven rendering starts like this:

function renderRoute(pathname) {
  if (pathname === "/dashboard") {
    renderDashboard();
    return;
  }
 
  if (pathname === "/settings") {
    renderSettings();
    return;
  }
 
  renderNotFound(pathname);
}
 
renderRoute(location.pathname);

That is already routing. It reads a URL part, chooses a view, and renders the matching UI.

The History API is the clean-URL pattern

The History API lets JavaScript add or replace entries in the browser's session history without loading a new document. The everyday method is history.pushState(stateObject, "", url).

In a regular same-origin page, a client-side router often looks like this:

const routes = {
  "/dashboard": renderDashboard,
  "/lessons": renderLessons,
  "/settings": renderSettings,
};
 
function renderPath(pathname) {
  const render = routes[pathname] ?? renderNotFound;
  render(pathname);
}
 
function navigate(pathname) {
  history.pushState({ pathname }, "", pathname);
  renderPath(pathname);
}
 
window.addEventListener("popstate", () => {
  renderPath(location.pathname);
});
 
renderPath(location.pathname);

Two details are easy to miss. First, pushState changes the URL and adds a history entry, but it does not fire popstate; your code renders immediately after pushing. Second, popstate fires when the active history entry changes, most commonly when the user presses Back or Forward.

History routing gives you clean URLs like /settings. The server has to cooperate: if the user refreshes /settings, the server must send the app shell instead of a 404. Without that server fallback, history routing works while clicking around, then breaks on refresh or direct links.

The live preview for this lesson is an opaque about:srcdoc page. It can safely update its hash, but it blocks history.pushState(...), so the runnable router below uses hash routing. The normal-page History API shape above is the one you will use in a real same-origin app.

Hash routing is the simple fallback

A hash route stores the current view after #, like #/settings or #settings. The hash is the URL fragment you met in Section 16: the browser keeps it client-side and does not send it to the server in the request.

Changing location.hash updates the address and fires a hashchange event. Back and Forward through hash entries also fire hashchange, which gives your router one clean place to re-read the URL and render again.

Hash routing trades URL cleanliness for deployment simplicity:

  • Hash routing works on static hosting and constrained previews because the server never needs to know the client route.
  • History routing gives cleaner URLs, but the server must serve the app shell for every route your client owns.

Both patterns still use the same state-render thinking: read the URL, update route state, render the matching view.

Try it - build a tiny hash router

Run the playground and click the view buttons. The URL fragment changes, hashchange reads it, and render(state) paints the matching screen. Click Missing route to see the fallback view.

web page — live previewsandboxed
Run rebuilds the page

The live router has the same shape as the task board from 17.2:

  1. Start with state: state = readRoute().
  2. Render from state.
  3. Listen for events.
  4. Change the URL when the user navigates.
  5. Re-read the URL on hashchange, update state, and render again.

The URL is not a decoration. It is the source for state.route. That is why the missing-route button works: the app reads #missing, sees no screen registered for it, and renders a not-found view instead of crashing.

What belongs in the URL?

Put state in the URL when the user expects the view to survive sharing, bookmarking, refreshing, or Back/Forward:

  • Current page or screen: #/settings
  • Selected tab in a meaningful report: #/reports/revenue
  • Search filters the user may share: ?team=studio&status=open

Keep state in memory when it is temporary or private to the current interaction:

  • Which field currently has focus
  • Whether a tiny menu is open
  • A half-typed draft before the user chooses to save it

URL state is a public address for a view. Memory state is the working desk while the user is there. Good apps use both.

The router is small because the ideas are old

A hand-built router needs only a few pieces: route definitions, a function that reads the current URL, a render function, and one event listener for URL changes. Hash routers listen to hashchange. History routers call pushState for clicks and listen to popstate for Back and Forward.

Once the app grows, you will usually let a framework router handle nested routes, data loading, scroll restoration, and server fallback rules. But the small version matters because it tells you what those routers are protecting: the URL is a contract with the user.

Part IV keeps widening from here. You have DOM, events, fetch, storage, state, and routing; next comes a tour of Web APIs that let a page react to layout, draw pixels, use media, and move work off the main thread.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion