Frontend Development 7 min read

Async/Await Decorators for Vue: Initialization Flag, Button Debounce with Loading Style, and First‑Paint Control

This article demonstrates three Vue decorators that use async/await to transparently manage component initialization status, add debounce and loading indicators to button clicks, and control the visibility of the component before the mounted hook finishes, with full TypeScript examples.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Async/Await Decorators for Vue: Initialization Flag, Button Debounce with Loading Style, and First‑Paint Control

The three decorators below exploit the async/await feature to turn asynchronous operations into synchronous‑like behavior; the decorated methods must be written with async/await, making the usage extremely convenient as the implementation is completely hidden inside the decorator.

1. Add a variable indicating that Vue initialization is complete

This decorator injects a pageIsReady property into a Vue component and sets it to true after both the created and mounted lifecycle hooks have finished. It works in a TypeScript class‑based Vue environment but can be adapted to plain JavaScript Vue components.

import { Constructor } from "vue/types/options");
export type WrapReadyProperty
= T & { pageIsReady?: boolean };
/**
 * Use this decorator after @component. The component will receive a pageIsReady property.
 * When both created and mounted have completed, pageIsReady becomes true.
 * The decorated methods must be async/await.
 */
export default function PageReadyStatus() {
    let createdDone = false;
    let mountedDone = false;
    function isCreatedMountedAllDone() {
        return createdDone && mountedDone;
    }
    return function pageReadyEnhancement
(target: T) {
        const oldMounted = target.prototype.mounted || function() {};
        const oldCreated = target.prototype.created || function() {};
        const oldBeforeDestroy = target.prototype.beforeDestroy || function() {};
        target.prototype.pageIsReady = false;
        target.prototype.created = async function(...params: any[]) {
            await oldCreated.apply(this, params);
            createdDone = true;
            this.pageIsReady = isCreatedMountedAllDone();
        };
        target.prototype.mounted = async function(...params: any[]) {
            await oldMounted.apply(this, params);
            mountedDone = true;
            this.pageIsReady = isCreatedMountedAllDone();
        };
        target.prototype.beforeDestroy = async function(...params: any[]) {
            await oldBeforeDestroy.apply(this, params);
            mountedDone = false;
            createdDone = false;
            this.pageIsReady = false;
        };
        return target;
    };
}
export function isPageReady(this: WrapReadyProperty
) {
    return this.pageIsReady;
}

2. Add debounce and loading style to event callbacks and button DOM

This decorator wraps an async method, prevents it from being called again while it is still pending, and temporarily changes the cursor of the clicked button to a waiting state. It is intended for TypeScript‑based Vue projects.

/*
 * Ensure the last parameter of the wrapped method is the click event.
 */
export default function buttonThrottle() {
    let pending = false;
    return function(target: any, name: string): any {
        const btnClickFunc = target[name];
        const newFunc = async function(this: Vue, ...params: any[]) {
            if (pending) { return; }
            const event: Event = params[params.length - 1];
            let btn = event.target as HTMLElement;
            pending = true;
            const recoverCursor = changeCursor(btn);
            try {
                await btnClickFunc.apply(this, params);
            } catch (error) {
                console.error(error);
            }
            recoverCursor();
            pending = false;
        };
        target[name] = newFunc;
        return target;
    };
}
function changeCursor(btn?: HTMLElement) {
    if (btn == null) { return () => {}; }
    const oldCursor = btn.style.cursor;
    btn.style.cursor = "wait";
    return () => { btn.style.cursor = oldCursor; };
}

Usage example: apply @buttonThrottle() to an async click handler; the decorator automatically detects when the handler finishes and adds a cursor: wait style to the button during execution.

import { Component, Vue } from "vue-property-decorator";
import buttonThrottle from "@/ui/view/utils/buttonThrottle";

type Member = { account_no: string; name: string; warn?: string };
@Component({ components: {} })
export default class AddMemberInput extends Vue {
    @buttonThrottle()
    private async confirmAdd() {
        await this.addMembers(this.getVaildMembers());
    }
}

3. Hide white screen before mounted

This decorator modifies a Vue instance so that its root element is hidden before the original mounted hook runs and becomes visible afterward, allowing developers to display custom loading content during the initial paint.

function firstPaintControl(vueObj) {
    let oldMounted = vueObj.mounted || function() {};
    vueObj.mounted = async function(...params) {
        this.$el.style.visibility = 'hidden';
        await oldMounted.apply(this, params);
        this.$el.style.visibility = 'visible';
    };
    return vueObj;
}
FrontendTypeScriptJavaScriptVueAsync/Awaitdecorator
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

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.