Home / Blog / Enhance Router To Work With Single Page Applications
Enhance Router To Work With Single Page Applications

Enhance Router To Work With Single Page Applications

Alex Jover Morales
Alex Jover Morales
Updated: May 15th 2022
This entry is part 2 of 4 in the series Creating Your Own Router

At this point, the router doesn't work as a Single Page App (SPA) router, since it assigns a new route to window.location directly, which performs a hard reload.

Let’s see what we have to do to adapt our router to serve as a SPA router.

Hash And HTML5 History

To cope with hard reloads, we have two alternatives, also known as modes:

  • Hash: it uses a hash to define fragments identifiers, which are the different routes from the SPA. This way, the front-end routing starts from the # mark. This is the traditional way to do it. An example is https://example.io/#/home
  • HTML 5 history: using the HTML 5 History API is the modern way to do it. This mode doesn't require a hash.

In this article we'll use the HTML 5 history mode and we’ll see along the way how to use the History API. However, if you’d like to go for the hash mode, it would require some minor changes, but it should be doable.

Implement A History Module

The History API has a pushState method that we can use as a replacement for the window.location assignment.

The pushState method, unlike the window.location assignment, doesn’t perform a hard reload which makes it perfect for the case.

We could update the App.vue goTo method accordingly:

// App.vue
goTo(route) {
  history.pushState(null, null, route)
}

We can omit the first two parameters (state and title) from the call, we're just interested in the route, received as a third parameter.

However, we still have a problem. Even if we set the route using pushState, we still need to know from the Router.vue component that the route has changed in order to update the current route variable.

If you’re already familiar with the History API, you’re probably thinking that you can use the onpopstate event, but if you read carefully you'll see that event is not triggered when pushState is called. We still need it to react to that change when the user presses the back and forth browsers buttons, but is not enough.

To solve that, we can create a history wrapper where we can listen to changes, and call that one instead. Let’s start by creating a history.js file and wrap the push method:

// history.js
export const push = route => {
    window.history.pushState(null, null, route);
  }
}

In order to make the history listenable, we need to implement a listen method that should store callback functions in a listeners variable, which external modules will use to be notified of the changes.

// history.js
const listeners = [];

export const push = route => {
  window.history.pushState(null, null, route);
};

export const listen = fn => {
  listeners.push(fn);
};

Finally, the push method should actually notify by iterating the listeners and call the callback functions passing the route. We could pass the previous route as well as a second argument:

push(route) {
  const previousRoute = window.location.pathname;
  window.history.pushState(null, null, route);
  listeners.forEach(listener => listener(route, previousRoute));
}

If this way of using listeners to notify external modules looks familiar to you it’s because history.js is implementing the Observer Pattern, a pattern that provides the structure to offer a subscription model where other components can listen to changes.

The final code is:

// history.js
const listeners = [];

export const push = route => {
  const previousRoute = window.location.pathname;
  window.history.pushState(null, null, route);
  listeners.forEach(listener => listener(route, previousRoute));
};

export const listen = fn => {
  listeners.push(fn);
};

The history.js module could be even used as an external package, published on npm. In fact, there is an existing history npm package that the React Router team created for React Router, but it’s an independent module and we could perfectly use it for this case, since the API is almost identical.

Use The History Module

Now history.js is implemented, let's use it in App.vue replacing the pushState call:

// App.vue
import Router from "./Router";
import { push } from "./history";

export default {
  components: {
    Router
  },
  methods: {
    goTo(route) {
     push(route);
    }
  }
};

We still need to make the AppRouter.vue component aware of the url changes, and we can now use the listen method from the history module. We just need to set the listener once, hence the created hook of Vue.js components is a perfect place to put it:

// AppRouter.vue
import { listen } from "./history";

// ...

export default {
  created() {
   listen((route, previousRoute) => {
      this.current = route
    });
  },
  // ...
}

The function passed to the listen statement will be added to history's listeners and then called later when push is called.

If you try it out right now and click on the Home and Articles buttons, it should work. However, it still doesn't work when you click the browsers back and forth buttons. As mentioned before, we must use the popstate event for that, so let's add it to the Router's created hook, finishing up in:

created() {
 listen((route, previousRoute) => {
    this.current = route
  });

  window.addEventListener(
    "popstate",
    event => (this.current = window.location.pathname)
  );
}

At last, if you try it again, it should work in all those cases, being a fully working simple router. From here, feel encouraged to add more features to it! What about adding hooks and guards?
Find here the code and demo from this article and try it yourself!

Related Courses

Start learning Vue.js for free

Alex Jover Morales
Alex Jover Morales
Passionate web developer. Author of Test Vue.js components with Jest on Leanpub. I co-organize Alicante Frontend. Interested in web performance, PWA, the human side of code and wellness. Cat lover, sports practitioner and good friend of his friends. His bike goes with him.

Comments

Latest Vue School Articles

Why Vue.js is a Great Starting Point for New Coders

Why Vue.js is a Great Starting Point for New Coders

Dive into Vue.js as a beginner with our comprehensive guide. Learn why Vue is perfect for starting your web development journey, with insights into its community, learning resources, and real-world uses.
Eleftheria Batsou
Eleftheria Batsou
The Vue Form Component Pattern: Robust Forms Without the Fuss

The Vue Form Component Pattern: Robust Forms Without the Fuss

Learn to create easy to use Vue forms without a library and take advantage of native HTML validation.
Daniel Kelly
Daniel Kelly

Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.

More than 200.000 users have already joined us. You are welcome too!

Follow us on Social

© All rights reserved. Made with ❤️ by BitterBrains, Inc.