Mark Ku's Blog

Vue global / non-global loading transitions (supporting concurrent requests)

What this solves

On the frontend, Ajax is constantly being used to exchange data asynchronously with the backend, and APIs may take varying amounts of time to respond. To make the experience feel better, we usually show a loading transition so users know data is being fetched.

Most libraries do this very simply — often just a boolean toggle. With multiple concurrent async requests, whichever one returns first turns off the loading transition for everyone.

Design idea

Build a counter in Vuex. When an Ajax request fires, call the store's increment so count goes up by 1; when it completes or fails, decrement by 1. Expose an isLoading getter for the UI to read. Wrap axios so the caller can pass a parameter that decides whether the transition should show or not.

  1. Request goes out → count + 1
  2. Request fails → count - 1 (parameter validation errors stop here and never reach the response)
  3. Response succeeds → count - 1
  4. Response fails → count - 1 (timeout, server error, cancel)

Vuex code

import { generatorUUID } from '@/utils'
var _ = require('lodash')

const state = {

  count: []
}
// getters
const getters = {
  isLoading: (state) => {
    return state.count.length > 0
  },
  removeByItems: (state) => {
    return state.count.filter(x => x.isRemoveByRequestId === true)
  }
}
// mutations
const mutations = {
  increment(state, requestId) {
    const isRemoveByRequestId = requestId ! '' && requestId ! undefined

    requestId = requestId || generatorUUID()

    var newLoading = { requestId: requestId, isRemoveByRequestId: isRemoveByRequestId }

    state.count.push(newLoading)
  },
  decrement(state, requestId) {
    if (requestId) {
      var index = state.count.findIndex(item => item.requestId === requestId)
      state.cart.splice(index, 1)
    } else {
      const newCount = _.cloneDeep(state.count).filter(x => x.isRemoveByRequestId === false)
      if (newCount.length > 0) {
        newCount.shift()
        state.count = newCount
      }
    }
  }
}
// actions
const actions = {

}
export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations
}

Wrapping axios

function apiAxios(method, url, params, hideLoading, requestId) {
  // 顯示loading
  const appParams = {}
  params = { ...appParams, ...params }
  const httpDefault = {
    method: method,
    baseURL: baseURL,
    url: url,
    hideLoading: hideLoading,
    requestId: requestId,
    // `params` 是即將與請求一起發送的 URL 參數
    // `data` 是作為請求主體被發送的數據
    params: method = 'GET' || method = 'DELETE' ? params : null,
    data: method = 'POST' || method = 'PUT' ? params : null,
    timeout: 100000
  }

  if (httpDefault['hideLoading'] === false) {
    store.commit('loading/increment', httpDefault['requestId'])
  }

  return new Promise((resolve, reject) => {
    axios(httpDefault)
      .then((res) => {
        if (httpDefault['hideLoading'] === false) {
          store.commit('loading/decrement', requestId)
        }
        resolve(successState(res))
      })
      .catch((response) => {
        if (httpDefault['hideLoading'] === false) {
          store.commit('loading/decrement', requestId)
        }

        errorState(response)
      })
  })
}

Add a loading-box component to the layout, watching the isLoading getter to decide when to display it.

Vue code demonstrating isLoading getter controlling LoadingBox
Vue code demonstrating isLoading getter controlling LoadingBox

To skip the loading transition for a particular API call, pass true as an extra argument

Vue.prototype.getAxios(getAxios(`{process.env.VUE_APP_BASE_API}/api/v1/home/query`, null, true)

To leave room for future flexibility — if a specific component needs to control loading, the caller can pass a request id, and the component can watch the isRemoveByRequestId property within the loading-count object

export function getHomeQuery(requestId = '') { return Vue.prototype.getAxios(getAxios(`{process.env.VUE_APP_BASE_API}/api/v1/home/query`, null, true, requestId) }

More and more sites adopt skeleton screens to improve the user experience; a good follow-up optimization to consider.

Reference link

Author

Mark Ku

擁有 10+ 年經驗的資深軟體工程師,現為 AI 應用 Builder,專注於大型平台架構與簡化複雜系統設計,從電商系統到訂閱與收費平台,結合 AI Agent、AI 整合與自動化開發,打造高效率且可持續演進的產品技術基礎。Read More

Found this useful?

The author's free tools, daily podcasts and newsletter are all here.

Mark Ku · This article is licensed under CC BY 4.0. Credit the author and link back to the original when reusing it.

Comments

Subscribe to Newsletter

Subscribe to get new posts delivered instantly — never miss a tech share.

By submitting, you agree to receive emails. You can anytime.

Popular Posts

View all
Mark Ku
··602

Oracle Cloud Always Free Tier: Linux Host and Static IP for a $0 Cloud Solution

Oracle Cloud Always Free Tier: Linux Host and Static IP for a $0 Cloud Solution
Mark Ku
··492

Say Goodbye to Postman's Fee Trap! A Hands-on Guide to Bruno, the Open-Source Git-Native API Testing Powerhouse.

Say Goodbye to Postman's Fee Trap! A Hands-on Guide to Bruno, the Open-Source Git-Native API Testing Powerhouse.
Mark Ku
··334

A Free, Open-Source, Notion-like Knowledge Base — A Complete Guide to Deploying and Backing Up Outline Wiki

A Free, Open-Source, Notion-like Knowledge Base — A Complete Guide to Deploying and Backing Up Outline Wiki
Mark Ku
··268

Training Your Own AI Voice: Hardware Requirements, Open-Source Model Comparison, and LoRA Fine-Tuning

Training Your Own AI Voice: Hardware Requirements, Open-Source Model Comparison, and LoRA Fine-Tuning
Mark Ku
··218

Building an Efficient API Management Platform: Deploying Kong Gateway from Scratch - Part 1

Building an Efficient API Management Platform: Deploying Kong Gateway from Scratch - Part 1
Mark Ku
··217

Setting Up Samba on Ubuntu to Share Folders with Windows 11

Setting Up Samba on Ubuntu to Share Folders with Windows 11