Routing Without a Framework: Building a Minimal SPA Router
Single Page Applications (SPAs) have become the go-to architecture for modern web applications, offering a seamless user experience by loading content dynamically without refreshing the entire page. However, many developers rely on heavy frameworks that may be overkill for smaller projects. In this blog, we’ll explore how to build a minimal router from scratch, achieving efficient navigation in an SPA without the need for a large framework.
Understanding the Basics of SPA Routing
Before diving into creating a router, it’s crucial to understand what routing is and how it works in the context of an SPA. Routing refers to the mechanism that allows users to navigate between different views or components within an application. In SPAs, this is usually achieved through client-side manipulation of the browser’s history API.
When a user clicks on a link, the browser URL updates, and the associated content for that route is fetched and displayed without a full page reload. This enhances user experience and reduces loading times significantly.
Setting Up Your Project
To get started, we’ll set up a simple HTML file and a JavaScript file. Here is a basic structure:
<!DOCTYPE html>
<html>
<head>
<title>My Minimal SPA</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
a { text-decoration: none; color: blue; }
.content { margin-top: 20px; }
</style>
</head>
<body>
<nav>
<a href="/" data-link>Home</a> |
<a href="/about" data-link>About</a> |
<a href="/contact" data-link>Contact</a>
</nav>
<div class="content"></div>
<script src="app.js"></script>
</body>
</html>
Create an HTML file as outlined, and ensure you have a JavaScript file named app.js.
Creating the Router
Now, let’s write our basic router in JavaScript. The router’s responsibility will be to handle navigation, render the appropriate content based on the URL, and manipulate the browser’s history.
const routes = {
'/': 'Home Page
<p>Welcome to our minimal SPA!</p>',
'/about': '<h2>About Us</h2><p>We are a small team of developers.</p>',
'/contact': '<h2>Contact Us</h2><p>You can reach us at [email protected].</p>',
};
function router() {
const content = document.querySelector('.content');
const path = window.location.pathname;
content.innerHTML = routes[path] || '<h2>404 Not Found</h2>';
}
window.addEventListener('popstate', router);
document.querySelectorAll('[data-link]').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const path = this.getAttribute('href');
window.history.pushState(null, '', path);
router();
});
});
// Initial load
router();
Code Explanation
Let’s break down the code we just wrote:
- Routes Definition: We created an object called routes that maps URL paths to HTML content. This object serves as a simple lookup table.
- Router Function: The router function retrieves the current path from the browser’s URL and updates the inner HTML of the content div based on the defined routes, or a 404 message if the path doesn’t exist.
- Popstate Event: This event triggers the router function when the user navigates back or forward in their browser history, ensuring the app responds appropriately.
- Link Click Handler: Each link with the data-link attribute has a click event listener. It prevents the default link behavior, updates the browser’s history state using pushState, and calls the router function to render new content.
Adding a 404 Not Found Page
It’s a good practice to provide feedback when an invalid path is requested. We have already added a basic 404 message in our router function. Still, we can enhance it further by creating a dedicated 404 component.
const routes = {
'/': '<h2>Home Page</h2><p>Welcome to our minimal SPA!</p>',
'/about': '<h2>About Us</h2><p>We are a small team of developers.</p>',
'/contact': '<h2>Contact Us</h2><p>You can reach us at [email protected].</p>',
'404': '<h2>404 Not Found</h2><p>Oops! The page you are looking for does not exist.</p>'
};
function router() {
const content = document.querySelector('.content');
const path = window.location.pathname;
if (routes[path]) {
content.innerHTML = routes[path];
} else {
content.innerHTML = routes['404'];
}
}
Enhancing the Router with Dynamic Paths
Our current router only handles static paths. To make it more versatile, we can support dynamic paths that allow for parameters, such as user IDs or product IDs.
const routes = {
'/': '<h2>Home</h2>',
'/user/:id': (id) => `<h2>User Profile</h2><p>User ID: ${id}</p>`,
};
function matchRoute(path) {
const routeKeys = Object.keys(routes);
for (const route of routeKeys) {
const regex = new RegExp(route.replace(/:id/, '(\w+)')); // Matches dynamic id
const match = path.match(regex);
if (match) {
return typeof routes[route] === 'function' ? routes[route](match[1]) : routes[route];
}
}
return routes['404'];
}
function router() {
const path = window.location.pathname;
const content = document.querySelector('.content');
content.innerHTML = matchRoute(path);
}
Dynamic Path Breakdown
In this updated example, we added a new route that includes a dynamic parameter, :id. The matchRoute function uses a regular expression to check if the requested path matches any of the defined routes. If a match is found, and if that route is a function, it gets called with the captured parameter.
Handling Scroll Restoration
By default, the SPA does not preserve the scroll position when navigating through different routes, which can be problematic. To enhance user experience, we should save and restore scroll positions.
const scrollPositions = {};
function router() {
const path = window.location.pathname;
const content = document.querySelector('.content');
scrollPositions[path] = window.scrollY; // Save the scroll position of the current path
content.innerHTML = matchRoute(path);
// Restore scroll position on navigation
window.scrollTo(0, scrollPositions[path] || 0);
}
Styling and User Experience
In addition to functionality, the visual appeal and user experience of your SPA are paramount. Consider adding CSS classes to your content for transitions and animations, enhancing the perceived performance and responsiveness of the application.
<style>
.fade-in { opacity: 0; transition: opacity 0.5s ease-in; }
.fade-in.active { opacity: 1; }
</style>
const router = () => {
const path = window.location.pathname;
const content = document.querySelector('.content');
content.innerHTML = matchRoute(path);
content.classList.add('fade-in', 'active');
}
Conclusion
Creating a minimal SPA router from scratch allows developers to have finer control over the routing logic without the complexities of full-fledged frameworks. This lightweight approach enables better performance, easier debugging, and a customizable user experience.
By following the steps outlined in this blog, you now have a solid foundation for a minimal SPA router. You can expand it further by adding features like guards for authentication, nested routes, and even an integrated state management solution.
Building your routing system can be an enlightening experience, helping you understand the underlying mechanics of web applications and enhancing your skills as a developer. So why not give it a try?
Happy coding!
