Frontend Development 9 min read

Preventing Duplicate Button Submissions with Debounce, Loading State, and a Vue 3 Directive

This article explains how repeated button clicks cause duplicate submissions, demonstrates using debounce and a loading‑disabled state to mitigate the issue, and shows how to encapsulate the solution into a reusable Vue 3 directive with complete code examples.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Preventing Duplicate Button Submissions with Debounce, Loading State, and a Vue 3 Directive

Introduction

In everyday development, buttons are often clicked repeatedly by testers, leading to duplicate data submissions, especially when the backend response is slow.

Frontend developer: "Can you stop clicking so fast?"

Tester: "I don't know if the click succeeded, so I click many times."

Project manager: "This is a bug; the front‑end and back‑end should discuss a fix."

Backend developer: "It's not my problem; the request is slow, front‑end should handle it."

Debounce

Adding a debounce function can limit how often a click handler is invoked.

// pseudo‑code
Click me repeatedly
// debounce implementation omitted
const handleSubmit = debounce(submit, 1000);

However, when the debounce interval (1 s) is shorter than the backend response time (5 s), testers can still trigger many requests within the waiting period.

Use Loading State

When the request starts, set a loading flag on the button and disable it; clear the flag after a successful response.

// pseudo‑code
Click me repeatedly
let loading = false;
function handleSubmit(){
  loading = true; // enable loading
  ajax('xxx/xxx/xxx').then(res => {
    if(res.code == 200){
      loading = false; // disable loading
    }
  })
}

This solves the immediate bug but raises a new question: should each page define its own loading variable?

Vue 3 Directive Packaging

To reuse the loading logic, we encapsulate it into a global Vue directive.

// main.js (entry file)
import { createApp } from 'vue';
import App from './App.vue';
import { bLoading } from './permission/loading';
const app = createApp(App);
bLoading(app); // global registration
// bLoading.js
import type { App } from 'vue';
let tag = null;
const className = `
  el-icon {
    --color: inherit;
    align-items: center;
    display: inline-flex;
    height: 1em;
    justify-content: center;
    line-height: 1em;
    position: relative;
    width: 1em;
    fill: currentColor;
    color: var(--color);
    font-size: inherit;
  }
`;
const i = `
`;
export function bLoading(app: App
) {
  app.directive('bLoading', (el, binding) => {
    if (typeof binding.value !== 'function') {
      throw new Error('Directive value must be a function');
    }
    el.addEventListener('click', () => {
      addNode(el); // add loading icon
      setTimeout(() => {
        binding.value(() => {
          cleanNode(el); // remove loading
        });
      });
    });
  });
}
function addNode(el) {
  if (el.firstElementChild.tagName === 'I') {
    tag = el.firstElementChild;
    el.removeChild(el.firstElementChild);
    el.insertAdjacentHTML('afterbegin', i);
  } else {
    el.insertAdjacentHTML('afterbegin', i);
  }
  el.setAttribute('disabled', true);
  rotate('loading');
}
function cleanNode(el) {
  el.removeAttribute('disabled');
  if (el.firstElementChild) {
    el.removeChild(el.firstElementChild);
    if (tag) {
      el.prepend(tag);
    }
  }
}
function rotate(id) {
  const element = document.getElementById(id);
  let angle = 0;
  const speed = 2;
  function rotate() {
    angle = (angle + speed) % 360;
    element.style.transform = `rotate(${angle}deg)`;
    requestAnimationFrame(rotate);
  }
  rotate();
}

Page Usage

<el-button type="primary" v-bLoading="(next) => handleSubmit(next)" :icon="Plus">Click me repeatedly</el-button>
import { Plus } from '@element-plus/icons-vue';
function handleSubmit(next){
  // simulate async request
  setTimeout(() => {
    // request returns 200
    next(); // clear loading
  }, 3000);
}

Conclusion

By turning the loading logic into a Vue directive, duplicate submissions are prevented, code duplication is reduced, and the solution becomes easily reusable across multiple pages.

frontendJavaScriptVueLoadingDebounceDirective
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.