Vue Router’s History Modes Untangled

Moein Mirkiani
6 min readJun 4, 2024

--

Vue Router History Modes

One of the first challenges we’ll face in creating a Vue application is to set the vue-router's history option. Some engineers would immediately mention that hash mode is bad for a website’s SEO, so we should use the HTML5 mode. Let’s clarify something right at the beginning, History mode in vue-router is not just about SEO, although it does affect it.

Regardless of which mode we choose, a Vue application will almost always return a simple index.html with a <div id="app"></div> inside its body tag and some additional meta scripts. Some engineers may presume that SEO cares about the content of a page and find it somewhat confusing why the history mode in vue-router affects SEO as well. It’s simply because SEO also cares about the URL structure of a request, and this is where the history mode makes a part of its impact. Now that we know what we are dealing with, let’s discuss why we are dealing with it. Eventually, we’ll have enough information to make our decision.

Why the history option exists?

When we use the word “history”, we mean the browser history, they are typically two arrows pointing to the left and right where you write your URL address (yes I needed to specifically describe them 😁).

In a Single Page Application (SPA) like those built with Vue.js, managing browser history can be a bit more complex because the page does not reload when navigating between views. This is where Vue Router and its history modes come into play, allowing you to manage browser history in a way that aligns with user expectations and enhances your application’s SEO. I’ll briefly cover the importance of browser history:

  • User Navigation: It allows users to navigate through their browser’s history using the back and forward buttons. This is a common user expectation when browsing the web, and not supporting it can lead to a confusing user experience.
  • Bookmarking and Sharing: Users can bookmark or share URLs to specific parts of your application. This is particularly important for social media sharing, SEO, and user engagement.
  • State Restoration: It can help in restoring the state of a previous page view. For example, if a user fills out a form on a page, then navigates away, and then goes back, the form inputs can be restored.
  • Analytics: It allows you to track user behavior on your site more accurately. You can track page views as users navigate through your application, which is essential for understanding user behavior and optimizing your site.

Vue Router’s History Modes

Here I’ll copy and paste some of vue-router's documentation, and try to simplify them to have a better understanding:

Hash Mode

The hash history mode is created with createWebHashHistory():

// router.js

import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
history: createWebHashHistory(),
routes: [
{
meta: {
title: "Dashboard",
},
path: "/dashboard",
name: "dashboard",
component: () => import("@/views/DashboardView.vue")
},
{
meta: {
title: "Tickets",
},
path: "/tickets",
name: "tickets",
component: () => import("@/views/TicketsView.vue")
}
]
})

It uses a hash character (#) before the actual URL that is internally passed. Because this section of the URL is never sent to the server, it doesn't require any special treatment on the server level. It does however have a bad impact on SEO.

That # is annoying, right? 😖

The hash mode is the default mode for the Vue Router. It uses the URL hash (#) to simulate a full URL. This means that everything after the # symbol in the URL represents the route’s path within your Vue application.

For example, in an application that has the same router.js config as above, if you try to access the “Dashboard” page, your URL structure will look like example.com/#/dashboard.

In this mode, when the URL changes, the page won’t be reloaded. This is because the part of the URL after the # (known as the hash fragment) is never sent to the server. Therefore, it doesn’t require any special treatment on the server level. But wait, if the page is not reloaded, then how does a new page is displayed when the URL changes? In a SPA website, all the resources are loaded and available on the client-side with the first API call. All Vue needs to do is map the hash fragment to its corresponding view component and replace it inside the index.html file to display the proper content.

Another question; what if our app needs to display some information that should be fetched through an API call after the URL is updated?

This is what happens in this scenario:

  • The user navigates to a new route, which corresponds to a different component.
  • Vue.js creates a new instance of the component associated with the new route.
  • As part of the component’s creation, Vue.js calls several lifecycle hooks. These are special methods that get called at various stages in the life of a component.
  • One of these lifecycle hooks is created(). This is often where you’d initiate the API request to fetch the new data. You can also use beforeRouteEnter or beforeRouteUpdate in Vue Router for fetching data before the route is entered or updated.
  • Once the data is fetched (typically in a then() block if you’re using Promises), you update your component’s data properties with the new data. Because Vue.js is reactive, it automatically updates the DOM to reflect these changes.

It is worth mentioning that there is no explicitly created() hook in Vue 3’s Composition API. Every function (API call) placed directly inside the Composition API will be automatically called upon the component’s creation.

Yup! That annoying # saves lives :)

HTML5 Mode

The HTML5 mode is created with createWebHistory() and is the recommended mode:

// router.js

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
history: createWebHistory(),
routes: [
{
meta: {
title: "Dashboard",
},
path: "/dashboard",
name: "dashboard",
component: () => import("@/views/DashboardView.vue")
},
{
meta: {
title: "Tickets",
},
path: "/tickets",
name: "tickets",
component: () => import("@/views/TicketsView.vue")
}
]
})

When using createWebHistory(), the URL will look "normal," e.g. example.com/dashboard.

This mode utilizes the browser’s History API and its pushState and replaceState methods to keep a record of that session’s navigation history. There is a problem here though, if you directly type example.com/dashboard in your address bar, you’ll get a 404 error. In Hash Mode, the fragment never made its way to the server, but in HTML5 mode it is being processed server-side and the server responds with a 404 error indicating that this route does not exist here!

To bypass this issue, we need to add a simple catch-all fallback route to our server. If the URL doesn’t match any static assets, it should serve the same index.html page that your app lives in. Now that we have received our infamous empty index.html in our front-end, Vue will take care of the rest and display the corresponding component to that route.

The rest of the rendering scenarios are identical to what we discussed in the hash mode.

Memory Mode

This is the least used option for history mode in Vue applications. The memory history mode doesn’t assume a browser environment and therefore doesn’t interact with the URL nor automatically triggers the initial navigation. This makes it perfect for the Node environment and SSR. It is created with createMemoryHistory() and requires you to push the initial navigation after calling app.use(router).

While it’s not recommended, you can use this mode inside Browser applications but note there will be no history, meaning you won’t be able to go back or forward.

This mode is primarily designed for environments that don’t have access to the browser’s URL, such as Node.js environments, or for Server-Side Rendering (SSR).

In memory mode, Vue Router maintains its own “virtual” history stack in JavaScript memory. This allows you to navigate through your application’s history via in-app controls like button clicks or links.

However, because this history is not tied to the browser’s history, the browser’s back and forward buttons won’t navigate through this “virtual” history. Instead, they’ll navigate through the browser’s own history, which doesn’t include the in-app route changes made in memory mode.

In memory mode, since the Vue Router doesn’t have access to the browser’s URL, it doesn’t know which route to load initially. Therefore, you need to manually specify the initial route that should be loaded when your application starts.

This is typically done using the router.push() method after creating your router. For example:

// router.js

import { createRouter, createMemoryHistory } from 'vue-router'

const router = createRouter({
history: createMemoryHistory(),
routes: [
{
meta: {
title: "Home",
},
path: "/",
name: "home",
component: () => import("@/views/HomeView.vue")
},
{
meta: {
title: "Dashboard",
},
path: "/dashboard",
name: "dashboard",
component: () => import("@/views/DashboardView.vue")
},
{
meta: {
title: "Tickets",
},
path: "/tickets",
name: "tickets",
component: () => import("@/views/TicketsView.vue")
}
]
})

router.push('/')

--

--