Logo Dave LobosDave Lobos About
Cover Image: Simple Single-Page App Routing with Vanilla JavaScript

Simple Single-Page App Routing with Vanilla JavaScript

A minimal example of implementing SPA routing to understand the core concepts behind the fancy frameworks and libraries, using only Vanilla JavaScript.

Single-Page Applications (SPAs) have become a standard in web development, offering a fluid user experience by dynamically updating content without full page reloads. At the core of every SPA is a routing system that manages what the user sees as they navigate through the application. In this article, we'll break down how to implement a barebones SPA router using nothing but Vanilla JavaScript.

This example is for educational purposes to illustrate the underlying mechanics, specifically the window.history.pushState method and the popstate event.

GitHub Gist

The Front-End: A Minimalist Router

Let's dive right into the code. Here's a complete HTML file that contains all the necessary JavaScript to create a simple client-side router.

<!doctype html>
<html>
  <head>
    <title>Vanilla JS Router</title>
    <meta charset="utf-8" />
    <meta http-equiv="content-type" content="text/html;charset=utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"/>
  </head>
  <body style="color:#eee; background-color:#222;">
    <script>
      const appRoot = document.createElement("div");
      
      appRoot.innerHTML = `
        <header>
          <a href="/section-one">Section One</a> <a href="/section-two">Section Two</a> <a href="/section-three">Section Three</a>
        </header>
        <main></main>
      `;
      
      const main = appRoot.querySelector("main");
      
      const updateUI = () => main.innerHTML = `Current section: ${window.location.pathname}`;
      
      appRoot.querySelectorAll("header > a").forEach(a => {
        const href = a.getAttribute("href");
      
        a.addEventListener("click", event => {
          event.preventDefault();
          if(href !== window.location.pathname){
            window.history.pushState(null, "", href);
            updateUI();
          }
        });
      });
      
      updateUI();
      
      window.addEventListener('popstate', updateUI);
      
      document.body.appendChild(appRoot);
    </script>
  </body>
</html>

So, what's happening here?

  1. We create a basic HTML structure with a header containing our navigation links and an empty <main> element where our content will be displayed.
  2. The updateUI function is our simple "view" logic. It takes the current path from window.location.pathname and displays it in the <main> element.
  3. We then select all the anchor tags in the header and attach a click event listener to each one.
  4. Inside the click event listener, we first call event.preventDefault() to stop the browser's default behavior of navigating to a new page.
  5. Next, we use window.history.pushState(null, "", href). This is the star of the show. It adds a new entry to the browser's session history stack, changing the URL in the address bar without triggering a page reload.
  6. After updating the URL, we call updateUI() to reflect the new "page" to the user.
  7. Finally, we add an event listener for the popstate event. This event is fired when the active history entry changes, for example, when the user clicks the browser's back or forward buttons. Our updateUI function is called here as well to ensure the content stays in sync with the URL.

The Back-End: The Supporting Act

For our front-end routing to work correctly, we need a web server that serves our index.html file for any requested route (/, /section-one, /section-two, etc.). This is a common requirement for SPAs.

Here’s a simple Go server that does just that:

package main

import (
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        http.ServeFile(w, r, "./index.html")
    })

    log.Println("Server started http://localhost:8080/")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

This Go program creates a web server that listens on port 8080. For every incoming request, regardless of the path, it responds by serving the index.html file. This ensures that our single-page application is always loaded, allowing our JavaScript router to take control of the user experience.

It's important to note that this server-side behavior is not exclusive to Go. You can achieve the same result with other technologies like NGINX, Node.js with Express, or any other back-end framework capable of handling URL rewriting.

The Takeaway: Know Your Fundamentals

While frameworks and libraries like React Router or Vue Router provide powerful and feature-rich solutions for routing, understanding the underlying browser APIs is crucial for any web developer. It demystifies what these tools are doing "under the hood" and empowers you to make informed decisions about your technology stack.

Sometimes, for smaller projects or specific use cases, a heavy framework is overkill. As programmers, we should always strive for simplicity. In many scenarios, a few lines of Vanilla JavaScript are all you need to implement a clean and effective solution.