How to make a progressive loading bar for your Vue app

Today I will teach you how to, in less than 100 lines of code, make a plugin for your Vue app to display the progress of async requests, similar to the ones seen on Medium and Github.

You can see a demo here, and the source code here.

Some bars will just show a random amount of progress to make the user “feel” like they are making progress. The one I will make is aimed at larger applications with a large number of async requests, so I’ll base the progress amount on the actual status (amount of completed requests vs total requests). Since it’s aimed at large apps, I’ll be handling all external requests by dispatching actions in a Vuex store.

Let’s get started. Create a new Vue app using the vue-cli:

vue init webpack vue-loader

You don’t need to use any test framework for this tutorial.

Next, create two files in src: store.js and plugin.js . We will separate the main logic into a plugin, since I’d like to eventually publish this as a package, and it’s good to know how to write plugins.

The way we will track progress is as follows:

  1. If I want to track the progress of a request, the dispatched action should have a isAjax: true option in the payload.
  2. If an action has a isAjax: true payload option, there should be a corresponding mutation that follows the below convention:

action: getAsyncData


So, the mutation should match the name of the action and append SUCCESS , and follow the convention of screaming snake case.

In App.vue, add a created method and inside it:

<div id="app">
v-for="s in Object.keys ($store.state.ajaxCalls)"
{{ $store.state.ajaxCalls[s] }}
{{ $store.getters['pendingCalls'] }} /
{{ $store.getters['total'] }}
export default {
name: 'App',
  created () {
this.$store.dispatch('firstCall', { isAjax: true })
this.$store.dispatch('secondCall', { isAjax: true })
this.$store.dispatch('thirdCall', { isAjax: true })

The above component dispatches three actions (which I will now create) and renders an object, $store.state.ajaxCalls, which we now create. All ongoing requests will be saved in this object. I also use two getters, to show the pending and total requests.

Add the following in store.js:

import Vue from 'vue'
import Vuex from 'vuex'
const state = {
ajaxCalls: {}
const mutations = {
SET_ACTION (state, { action }) {
state.ajaxCalls = {
...state.ajaxCalls, [action.type]: {
...action, pending: true
SET_PENDING (state, { type, pending }) {
state.ajaxCalls[type].pending = false
const actions = {
firstCall ({ commit }) {
setTimeout(() => {
}, 1000)
secondCall ({ commit }) {
setTimeout(() => {
}, 2500)
thirdCall ({ commit }) {
setTimeout(() => {
}, 4000)
const getters = {
pendingCalls: state => Object.keys(state.ajaxCalls)
.filter(x => state.ajaxCalls[x].pending === true).length,
  total: state => Object.keys(state.ajaxCalls).length
const store = new Vuex.Store({
strict: true,
state, mutations, actions, getters,
plugins: []
export default store

Now we have some actions set up, using setTimeout to simulate an external API taking time to respond. There is also a SET_ACTION mutation, that adds an action to the ajaxCalls object, and sets the pending flag to true. Lastly, for each action, we created a corresponding SUCCESS mutation. Even though the body is empty, it is necessary, since I will use the the Vuex store’s subscribe method to set pending to false when the action’s corresponding mutation is called.

Now we simply need to:

  1. When an action is dispatched, call SET_ACTION , which sets the action’s pending flag to true.
  2. When ACTION_TYPE_SUCCESS is committed, set pending to false.

Vuex exposes a subscribe and subscribeAction method, which are triggered every time a mutation or action is called. subscribeAction is relatively new — it came with Vuex 2.5.

Inside of plugin.js add the following:

import camelCase from 'lodash/camelCase'
export default store => {
store.subscribe((mutation, state) => {
if (mutation.type.includes('SUCCESS')) {
let type = camelCase(mutation.type.substring(0, mutation.type.indexOf('SUCCESS') - 1))
store.commit('SET_PENDING', { type, pending: false })
store.subscribeAction(action => {
if (action.payload && action.payload.isAjax === true)
store.commit('SET_ACTION', { action })

subscribeAction checks if the payload has the isAjax option, and adds the action to the ajaxCalls object with and sets pending to false.

subscribe checks if the mutation has SUCCESS appended, which we outlined as a convention above. If so, it commits the mutations, and sets the pending flag to false. Handling errors is something I’ll implement in the near future.

I’ll head back to store.js , add import progressiveLoader from './plugin to the top, and where I create the store, inject the plugin like so:

const store = new Vuex.Store({
strict: true,
plugins: [progressiveLoader]

If you run the app now, you should see something like this:

And as the setTimout methods execute, the pending flags change. Great! Now we just need a simple interface. You can style this however you like, but here’s a simple one to get started. Update App.vue (omitted code for brevity):

<div class="loading-bar">
<div class="progress" :style="width"></div>
/* ... */
computed: {
width () {
return {
'width': 100 - (100 / (this.$store.getters['total'] / this.$store.getters['pendingCalls'])) + 'vw',
'transition-duration': '1s'
.loading-bar {
left: 0px;
height: 3px;
top: 0px;
position: absolute;
width: 100vw;
background-color: green;
.progress {
background-color: white;
position: absolute;
left: 1px;
top: 1px;
height: 1px;

In width, I calculate the percentage of requests completed vs pending, and then show a bar based on that percentage. Adding transition-duration: 1s makes it look slick.

Again, You can see a demo here, and the source code here.

I will be extending this into a much more versatile plugin in the future, but this serves as a good guide on how to get started writing Vuex plugins. Some good improvements that I’d like to make (or have someone else contribute to)

  • better API to allow the user to style the progress bar?
  • handle errored requests
  • handle more complex scenarios (I’m sure there are some)
  • Handle cancelled requests

Thanks for reading!

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.