Reusable debounce function for Vue

Debounce operations in Vue using this 10 line script. Avoid loadash and friends.

What is debounce?

Delay and throttle operations to wait and collect user input before doing something with the input. The “something” can be updating another field, doing an API call, or starting a timer to self-destruct.

Did you not debounce earlier?

Yes, I did.

But, not in Vue.

I may have 365 articles on the blog, but I never forget. [don’t look back, and don’t make faces at the computer].

Let’s reuse the reusable debounce function to make something in Vue.

Use case

We have a registration screen that has a user id, which is unique to the application. User should be able to see if an id is taken at the time of typing the id. Nothing fancy - standard web application.

Get going with global debounce function in Vue

The mixin is globally accessible, but not “truly” global. We will try to not occupy the global space - we don’t expect every other field to be debounced.

Create a mixin

A Vue mixin allows us to incorporate parts that can be reused across components. Mixins allow us to selectively use methods, directives etc. in our components. If needed we could go full-steam and register a mixin globally as well, but we will not do that today.

We will use our debounce function in a mixin so that we can just call the darn thing from wherever.

PS: We can also create a Vue plugin to promote reusability. But, as Plato once said - “why a plugin when there’s a mixin”.

// ./mixins/PgtUtilMix.js
export default {
  methods: {
    pgtDebounce(func, delay) {
      let debounceTimer;
      return function() {
        console.log("debouncing call..");
        const context = this;
        const args = arguments;
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(() => func.apply(context, args), delay);
        console.log("..done");
      };
    }
  }
};

You are now free to use the method from your components.

Mix the mixin

Create a registration form in a view aptly called Register.vue. The below example uses Vuetify, but style libraries do not matter.

<!-- ./Views/Register.vue -->
<template>
  <v-container grid-list-md text-xs-center class="pt-5">
    <v-form ref="form" v-model="validInput">
      <v-row>
        <v-col cols="12" md="6" offset-md="3" class="pt-3">
          <v-text-field
            :value="registerUserId"
            autocomplete="userid"
            label="Enter your desired user id"
            hint="Used to logging into application and has to be unique."
            :rules="[rules.required]"
            @input="setRegisterUserId"
          >
            <template slot="append">
              <v-icon color="success" v-if="validUserIdStatus==true"
                >mdi-check</v-icon
              >
              <v-icon color="error" v-else-if="validUserIdStatus==false"
                >mdi-check</v-icon
              >
              <v-icon>mdi-account</v-icon>
            </template>
          </v-text-field>
        </v-col>

        <v-col cols="12" md="6" offset-md="3" class="pt-3">
          <v-text-field
            name="password"
            autocomplete="password"
            :value="registerPass"
            label="Password"
            :append-icon="value ? 'mdi-eye' : 'mdi-eye-off'"
            @click:append="() => (value = !value)"
            :type="value ? 'password' : 'text'"
            :rules="[rules.required]"
            @input="setRegisterPass"
          ></v-text-field>
        </v-col>

        <v-col cols="12"></v-col>
        <v-col offset-lg="6" lg="3" class="pt-3 text-right">
          <v-btn outlined to="/" class="mr-3">Cancel</v-btn>
          <v-btn color="primary" @click="validateAndRegister">Sign up</v-btn>
        </v-col>
      </v-row>
    </v-form>
  </v-container>
</template>

<script>
  import Panel from "../components/Panel";
  import { mapState, mapMutations, mapActions } from "vuex";
  import PgtUtilMix from "../mixins/PgtUtilMix";

  export default {
    components: { Panel },
    data() {
      return {
        value: String,
        validInput: true,
        rules: {
          required: value => !!value || "Required."
        }
      };
    },
    mixins: [PgtUtilMix],
    computed: {
      ...mapState("authentication", [
        "registerPass",
        "registerUserId",
        "registerError",
        "validUserIdStatus"
      ])
    },
    methods: {
      ...mapMutations("authentication", [
        "setRegisterUserId",
        "setRegisterPass"
      ]),

      ...mapActions("authentication", ["register", "checkUserIdValid"]),

      validateAndRegister() {
        if (this.$refs.form.validate()) {
          this.register();
        }
      }
    },

    watch: {
      registerUserId: function(val) {
        if (val) this.checkUserIdValid();
      }
    },

    mounted() {
      this.setRegisterPass("");

      this.checkUserIdValid = this.pgtDebounce(this.checkUserIdValid, 1000);
    }
  };
</script>

Our Vuex module will be standard enough - will not bother you with all details..

..except the function that calls an API on server to check whether the user id already exists -

// ./store/authentication.js

export default {
  // .. lot of other code

  actions: {
    // .. even more  code
    checkUserIdValid({ commit, state }) {
      return HTTP()
        .post("v0/auth/checkUserIdValid", {
          userid: state.registerDetails["registerUserId"]
        })
        .then(({ data }) => {
          commit("setValidUserIdStatus", data.valid);
          // true if id not present
        })
        .catch(e => {
          console.log("error", e);
        });
    }
  }
};

And, ta da..

vue-form-debounce

Dissecting our reusable debounce

We have used a standard Vue mixin to begin with ..

<script>
  import PgtUtilMix from "../mixins/PgtUtilMix";
  <!-- ... -->

  mixins: [PgtUtilMix]

  <!-- ... -->
</script>

We could have used debounce as-is, but you may have observed one complication - our function uses context to set this. We need to retain the component context for the reusable function to work.

So, we just override the original function with our debounce function.

// ...
    mounted() {
      //  ...
      this.checkUserIdValid = this.pgtDebounce(this.checkUserIdValid, 1000);
    }

// ...

Now, whenever we call checkUserIdValid we are referring to our streamlined super-function that simply calls the underlying function after introducing a delay of 1000ms.

We can use this debounce anywhere in our application as long as we import and refer to the mixin.

comments powered by Disqus