loading...
Cover image for Build a custom SPA Router using VanillaJS

Build a custom SPA Router using VanillaJS

skaytech profile image skaytech ・6 min read

Introduction

In this article, I will explain how I had built a custom SPA router using Vanilla JavaScript. I had to build a UI project without any using framework and had to figure how to handle routing and discovered that you can build your own router using Vanilla JavaScript.

Disclaimer

I completely agree on the philosophy that we should not spend time on problems that have been solved well previously and with the advent of frameworks, there are many ready made routers available that should be used. The intention of this article is to explain that it is possible to write a custom router using VanillaJS and it also gives a good understanding of what lies beneath.

Window - History & Location Objects

In order to build a custom router, we need to first understand the 'history' and the 'location' objects of the 'window' object and few methods that are required to handle the page navigation.

History Object

The window.history object provides the details regarding the browser's session history. It contains methods & properties that help you navigate back and forth through the user's history.

You can open your browser console and type history, and you'll see all the methods and properties of the history object listed as shown below.

Alt Text

Location Object

The window.location contains all the information related to the current location such as the origin, pathname, etc.

You can open your browser console and type location, and you'll see all the various properties and methods associated with the location object as shown below.

Alt Text

History - pushState()

The method pushState is used to add a state to the browser's session history stack.

Syntax: history.pushState(state, title, [, url]);

  • state - The JavaScript object associated with the new history entry. The state object can be anything that can be serialized.
  • title - The title is actually not used by Modern browsers yet. it is safe to pass an empty string or the title you wish you refer your state.
  • url - The new history entry's URL is specified by this parameter.

We will be using the pushState method to update the browser's URL during page navigation.

Window - popstate event

The popstate event) is fired when the active history changes when the user navigates the session history.

In other words, whenever a back or a forward button is pressed on the browser, then the history changes and at that moment the popstate event is fired.

We will be using the popstate event to handle logic whenever the history changes.

Implementing the Router

Now that we've got the fundamentals in place, we will look at a step-by-step approach to implementing the router using VanillaJS.

The View

The index.html is a very simple page which contains an unordered list of links for the pages -

  • home
  • about
  • contact

In addition, there are 3 separate HTMLs for the home, about and contact views.

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vanilla JS Router</title>
  </head>
  <body>
    <ul class="navbar-list">
      <li class="navbar-item">
        <a href="#" onclick="onNavClick('/about'); return false;">About</a>
      </li>
      <li class="navbar-item">
        <a href="#" onclick="onNavClick('/'); return false;">Home</a>
      </li>
      <li class="navbar-item">
        <a href="#" onclick="onNavClick('/contact'); return false;">Contact</a>
      </li>
    </ul>
    <div id="root"></div>
    <script src="./js/app.js"></script>
  </body>
</html>

home.html

<div>
  <h1>******Welcome to the Home Page*****</h1>
</div>

about.html

<div>
  <h1>******Welcome to the About Page*****</h1>
</div>

contact.html

<div>
  <h1>******Welcome to the Contact Page*****</h1>
</div>

Load the HTML pages (Async)

I have used the async/await with 'fetch API' for asynchronous loading of pages and have used 'promise' to assign the values to home, about and contact variables.

//Declare the variables for home, about & contact html pages
let home = '';
let about = '';
let contact = '';

/**
 *
 * @param {String} page - Represents the page information that needs to be retrieved
 * @returns {String} resHtml - The Page's HTML is returned from the async invocation
 */

const loadPage = async (page) => {
  const response = await fetch(page);
  const resHtml = await response.text();
  return resHtml;
};

/**
 * The Async function loads all HTML to the variables 'home', 'about' & 'contact'
 */
const loadAllPages = async () => {
  home = await loadPage('home.html');
  about = await loadPage('about.html');
  contact = await loadPage('contact.html');
};

Let us walk through the flow for one page:

  • When the 'loadAllPages' function is invoked, the first function loadPage('home.html') first fired.
  • Inside the 'loadPage' function, the fetch('home.html') will be fired to load the home.html asynchronously.
  • The 'await' keyword ensures that the 'response' variable is populated and the 'resHtml' is assigned 'response.text()' since text is returned in the API call.
  • The value of 'resHtml' is returned to the 'loadAllPages' function and assigned to the 'home' variable.

Likewise, API calls are made for 'about' and 'contact' pages as well and the values are populated to the variables about & contact.

The Main Function & Root Element

Fetch the 'rootDiv' from the 'index.html' document.

The main function will be invoked on the Page load. Inside, the main function, we are first ensuring that all the HTML pages are loaded into the variables 'home', 'about' and 'contact'.

In order to ensure that the 'home' page is loaded to the root element upon page load, the rootDiv.innerHTML is set to 'home' variable.

Further, the 'routes' are setup with the corresponding page mapping in order to load the appropriate page when the routes are called.

//Get the Element with the Id 'root'
const rootDiv = document.getElementById('root');

/**
 * The Main Function is an async function that first loads All Page HTML to the variables
 * Once the variables are loaded with the contents, then they are assigned to the 'routes' variable
 */
const main = async () => {
  await loadAllPages();
  rootDiv.innerHTML = home;
  routes = {
    '/': home,
    '/contact': contact,
    '/about': about,
  };
};

// Invoke the Main function
main();

Routing - When a link is clicked on the Main page

From the above index.html, we are invoking the 'onNavClick' method and passing in the 'route' upon clicking the 'a' link as shown in the code snippet below.

<li class="navbar-item">
    <a href="#" onclick="onNavClick('/about'); return false;">About</a>
</li>
/**
 *
 * @param {String} pathname - Pass the 'pathname' passed from onClick function of the link (index.html)
 * The function is invoked when any link is clicked in the html.
 * The onClick event on the html invokes the onNavClick & passes the pathname as param
 */
const onNavClick = (pathname) => {
  window.history.pushState({}, pathname, window.location.origin + pathname);
  rootDiv.innerHTML = routes[pathname];
};

The onNavClick method accepts the 'pathname' which is the 'route' link and uses the window.history.'pushState' method to alter the state.

The second line 'rootDiv.innerHTML = routes[pathname]' will render the appropriate page based on what is configured within the routes in the main function (see above).

At this point, you have a functional router that navigates to the appropriate page upon clicking a link and the corresponding link is also updated in the URL browser.

The only thing that you'll notice is that when you hit a 'back' or 'forward' button on the browser, the links are correctly updated on the URL, however the contents on the page are not refreshed.

Let us take care of that in the last section of the article.

Handle Page Rendering upon State Change

If you would recall from the above definition of 'onpopstate event' method, it'll be invoked whenever the active history changes in the browser.

We are using that hook to ensure that the rootDiv is populated with the appropriate page based on the routes configured.

That's it!! You should now have a fully functional custom router all built using Vanilla JavaScript.

/**
 * The Function is invoked when the window.history changes
 */
window.onpopstate = () => {  
  rootDiv.innerHTML = routes[window.location.pathname];
};

If you would like the complete code, you can find it on Github over here.

Conclusion

To summarize, we've covered how to build a basic custom router using VanillaJS. The router uses the window's history and location objects primarily and the methods pushState & onpopstate event.

Hope you enjoyed this article. Do let me know your feedback and comments.

You might also be interested in:

Posted on by:

skaytech profile

skaytech

@skaytech

Engineering Manager/Product Manager & an amateur Musical Keyboardist...

Discussion

markdown guide
 

I loved the article for two reasons

  1. You have a good way of explaining topics
  2. Even I meddled with the same idea three years ago and build RouteNow. Primarily for learning how it all works and then continued building it coz it saved me from boilerplate solutions out there lot of which is bloated

    GitHub logo akhilarjun / RouteNow

    RouteNow is a small fast library ⚡ that will help you in developing a SinglePage Application without any dependencies like jQuery, AngularJs, vue.js or any of those bulky frameworks.

    RouteNow

    Contributions-Welcome experimental File-Size Version

    A new revolutionary small router with no bullshit!

    RouteNow is a small library that will help you in developing a SinglePage Application without any dependencies like jQuery, AngularJs, vue.js or any of those bulky frameworks.

    Installation

    Installating RouteNow.js is as easy as it gets.

    Add the script to the end of your body

    <script src="../pathto/route-now.js"></script>

    Or use the CDN link

    
    <script src="https://cdn.jsdelivr.net/gh/akhilarjun/RouteNow@latest/js/route-now.js"></script>
    <!-- For specific version use the version tag -->
    <script src="https://cdn.jsdelivr.net/gh/akhilarjun/RouteNow@v2.0.1/js/route-now.js"></script>
    <!-- For Minified -->
    <script src="https://cdn.jsdelivr.net/gh/akhilarjun/RouteNow@v2.0.1/js/route-now.min.js"></script>

    Use anchor tags with r-href property to point to your paths

    <a r-href="home">Home</a>

    Now add the Router outlet where the views should…

 

Thanks Akhil! I'll checkout your repo. Thanks for sharing. I generally don't believe in reinventing the wheel. However, for personal projects it'll be a good idea to play around with the toolset of our choice.

 

Opps, I found this a lot interesting and helpful🙆‍♂️

Thanks😧