
Simple Single-Page App Routing with 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.
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.
<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?
- We create a basic HTML structure with a header containing our navigation links and an empty
<main>
element where our content will be displayed. - The
updateUI
function is our simple "view" logic. It takes the current path fromwindow.location.pathname
and displays it in the<main>
element. - We then select all the anchor tags in the header and attach a click event listener to each one.
- Inside the click event listener, we first call
event.preventDefault()
to stop the browser's default behavior of navigating to a new page. - 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. - After updating the URL, we call
updateUI()
to reflect the new "page" to the user. - 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. OurupdateUI
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:
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.