229

I have a simple input box in a Vue template and I would like to use debounce more or less like this:

<input type="text" v-model="filterKey" debounce="500">

However the debounce property has been deprecated in Vue 2. The recommendation only says: "use v-on:input + 3rd party debounce function".

How do you correctly implement it?

I've tried to implement it using lodash, v-on:input and v-model, but I am wondering if it is possible to do without the extra variable.

In template:

<input type="text" v-on:input="debounceInput" v-model="searchInput">

In script:

data: function () {
  return {
    searchInput: '',
    filterKey: ''
  }
},

methods: {
  debounceInput: _.debounce(function () {
    this.filterKey = this.searchInput;
  }, 500)
}

The filterkey is then used later in computed props.

kissu
  • 40,416
  • 14
  • 65
  • 133
MartinTeeVarga
  • 10,478
  • 12
  • 61
  • 98
  • 2
    Try this one http://stackoverflow.com/questions/41230343/how-to-temporize-the-analysis-of-an-input-field/41232221#41232221 – sobolevn Feb 13 '17 at 09:02
  • 3
    I would suggest to carefully read: https://vuejs.org/v2/guide/migration.html#debounce-Param-Attribute-for-v-model-removed – Marek Urbanowicz Feb 13 '17 at 09:37
  • 4
    There is an example in the guide: https://vuejs.org/v2/guide/computed.html#Watchers – Bengt Feb 13 '17 at 10:40

18 Answers18

229

I am using debounce NPM package and implemented like this:

<input @input="debounceInput">
methods: {
    debounceInput: debounce(function (e) {
      this.$store.dispatch('updateInput', e.target.value)
    }, config.debouncers.default)
}

Using lodash and the example in the question, the implementation looks like this:

<input v-on:input="debounceInput">
methods: {
  debounceInput: _.debounce(function (e) {
    this.filterKey = e.target.value;
  }, 500)
}
kissu
  • 40,416
  • 14
  • 65
  • 133
Primoz Rome
  • 10,379
  • 17
  • 76
  • 108
  • 13
    Thanks for this. I found a similar example in some other Vue docs: https://vuejs.org/v2/examples/index.html (the markdown editor) – MartinTeeVarga Feb 13 '17 at 23:40
  • 13
    Proposed solution has a problem when there are several component instances on the page. Problem is described and solution presented here: https://forum.vuejs.org/t/issues-with-vuejs-component-and-debounce/7224/10 – Valera Jan 25 '18 at 02:30
  • e.currentTarget is overwritten to null this way – ness-EE Sep 12 '18 at 15:54
  • Thanks, you helped me a lot with my vue.js component… – Matt Komarnicki Jan 07 '19 at 23:56
  • 2
    Would recommend to add a `v-model=your_input_variable` to the input and in your vue `data`. So you do not rely on `e.target` but use Vue so you can access `this.your_input_variable` instead of `e.target.value` – DominikAngerer Jan 11 '19 at 12:16
  • 14
    For those using ES6, it's important to emphasize the use of the anonymous function here: if you use an arrow function you will not be able to access `this` within the function. – Polosson Mar 31 '20 at 15:04
  • no. OP asked for a vue html property, as is best for this. See: https://stackoverflow.com/a/50347709/1031191 (not my answer, that is the cleanest so far, even though it has only 5 vote-ups. It deserves a lot more, I think...) – Barney Szabolcs May 19 '20 at 11:56
  • @Polosson Why does it need to be an anonymous function? Seems like an arrow function would let you get the correct `this`. I've implemented the solution above exactly with an arrow function and without, and I'm running into `this` issues in both cases. Not sure what's wrong. – Tom T Oct 20 '21 at 22:56
165

Option 1: Re-usable, no deps

- Recommended if needed more than once in your project

/helpers.js

export function debounce (fn, delay) {
  var timeoutID = null
  return function () {
    clearTimeout(timeoutID)
    var args = arguments
    var that = this
    timeoutID = setTimeout(function () {
      fn.apply(that, args)
    }, delay)
  }
}

Typescript?

export function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
  let timeoutID: number | null = null;

  return function (this: any, ...args: any[]) {
    clearTimeout(timeoutID);
    timeoutID = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  } as T;
}

Or if using a d.ts:

declare function debounce(fn: (...args: any[]) => void, delay: number): (...args: any[]) => void;

/Component.vue

<script>
  import {debounce} from './helpers'

  export default {
    data () {
      return {
        input: '',
        debouncedInput: ''
      }
    },
    watch: {
      input: debounce(function (newVal) {
        this.debouncedInput = newVal
      }, 500)
    }
  }
</script>

Codepen


Option 2: In-component, also no deps

- Recommended if using once or in small project

/Component.vue

<template>
    <input type="text" v-model="input" />
</template>

<script>
  export default {
    data: {
        timeout: null,
        debouncedInput: ''
    },
    computed: {
     input: {
        get() {
          return this.debouncedInput
        },
        set(val) {
          if (this.timeout) clearTimeout(this.timeout)
          this.timeout = setTimeout(() => {
            this.debouncedInput = val
          }, 300)
        }
      }
    }
  }
</script>

Codepen

digout
  • 4,041
  • 1
  • 31
  • 38
106

Assigning debounce in methods can be trouble. So instead of this:

// Bad
methods: {
  foo: _.debounce(function(){}, 1000)
}

You may try:

// Good
created () {
  this.foo = _.debounce(function(){}, 1000);
}

It becomes an issue if you have multiple instances of a component - similar to the way data should be a function that returns an object. Each instance needs its own debounce function if they are supposed to act independently.

Here's an example of the problem:

Vue.component('counter', {
  template: '<div>{{ i }}</div>',
  data: function(){
    return { i: 0 };
  },
  methods: {
    // DON'T DO THIS
    increment: _.debounce(function(){
      this.i += 1;
    }, 1000)
  }
});


new Vue({
  el: '#app',
  mounted () {
    this.$refs.counter1.increment();
    this.$refs.counter2.increment();
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js"></script>

<div id="app">
  <div>Both should change from 0 to 1:</div>
  <counter ref="counter1"></counter>
  <counter ref="counter2"></counter>
</div>
kissu
  • 40,416
  • 14
  • 65
  • 133
bendytree
  • 13,095
  • 11
  • 75
  • 91
  • 2
    Could you explain why assigning debounce in methods can be trouble? – MartinTeeVarga Apr 16 '18 at 12:11
  • 18
    See Example links are prone to link-rot. It's better to explain the problem in the answer - it will make it more valuable for the readers. – MartinTeeVarga Apr 16 '18 at 23:41
  • Thank you very match, i had a bad time trying to understand why the data displayed on the console was right but not applied on the app ... –  Sep 04 '18 at 14:19
  • @sm4 because instead of using the same shared debounced instance for your desired function, it recreates it each time, thus killing the use of debounce mainly. – Mike Sheward Nov 01 '18 at 11:46
  • Thanks I was looking for this answer. Apparently when you make a `_.debounce` of the same function in copies of a component, only the last created component will be called. – Flame Jan 11 '19 at 10:34
  • 3
    just add it to your `data()` then. – Suau Mar 13 '19 at 08:50
  • Just wasted an hour debugging this! – Kshitiz Nov 21 '19 at 16:49
  • this is actually wrong. You don't need to declare methods in lifecycle hooks as they are linked to their own instance. Not sure where OP got this idea from, but yeah he's 100% wrong. – Hybrid web dev Apr 23 '20 at 01:28
  • My vote got locked in. The best solution so far is actually the one with 5 vote-ups: https://stackoverflow.com/a/50347709/1031191 because it neatly decouples the debounce logic from the container. – Barney Szabolcs May 19 '20 at 11:54
  • 1
    @Hybridwebdev I reckon he got it from Linus Borg's answer from the Vue forum, so I would say that this is the correct solution https://forum.vuejs.org/t/lodash-debounce-not-working-when-placed-inside-a-method/86334/5 – ness-EE Jun 21 '21 at 17:50
  • Thank you! This way I can access `this` inside the debounced function – Luis Cabrera Benito Jul 16 '21 at 21:01
  • i just encountered this case. you are 3 years ahead of me haha. thankyou random people – Gery Ruslandi Nov 06 '21 at 08:42
82

Very simple without lodash

handleScroll: function() {
  if (this.timeout) 
    clearTimeout(this.timeout); 

  this.timeout = setTimeout(() => {
    // your action
  }, 200); // delay
}
kissu
  • 40,416
  • 14
  • 65
  • 133
pshx
  • 897
  • 1
  • 8
  • 9
  • 12
    As much as I love lodash, this is clearly the best answer for a trailing debounce. Easiest to implement as well as understand. – Michael Hays Mar 02 '20 at 20:19
  • 7
    also is a good thing to add `destroyed() { clearInterval(this.timeout) }` in order to not having a timeout after destroyed. – pikilon May 21 '20 at 17:17
  • i am not sure how to use this when text changes on an input field. Can someone show an example? – Just a coder Nov 03 '21 at 11:45
  • @Justacoder you need to add an event listener to the input. Google `input addEventListener` – brad Nov 05 '21 at 14:49
12

I had the same problem and here is a solution that works without plugins.

Since <input v-model="xxxx"> is exactly the same as

<input
   v-bind:value="xxxx"
   v-on:input="xxxx = $event.target.value"
>

(source)

I figured I could set a debounce function on the assigning of xxxx in xxxx = $event.target.value

like this

<input
   v-bind:value="xxxx"
   v-on:input="debounceSearch($event.target.value)"
>

methods:

debounceSearch(val){
  if(search_timeout) clearTimeout(search_timeout);
  var that=this;
  search_timeout = setTimeout(function() {
    that.xxxx = val; 
  }, 400);
},
kissu
  • 40,416
  • 14
  • 65
  • 133
stallingOne
  • 3,633
  • 3
  • 41
  • 63
  • 1
    if your input field also had an ```@input="update_something"``` action then call this after ```that.xxx = val``` ```that.update_something();``` – Neon22 Jun 23 '19 at 09:33
  • 1
    in my methods section I used a slightly different syntax which worked for me: ```debounceSearch: function(val) { if (this.search_timeout) clearTimeout(this.search_timeout); var that=this; this.search_timeout = setTimeout(function() { that.thread_count = val; that.update_something(); }, 500); }, ``` – Neon22 Jun 23 '19 at 09:34
  • This is ok if you're having one or very few instances where you need to debounce input. However, you'll quickly realize you'll need to move this to a library or similar if the app grows and this functionality is needed elsewhere. Keep your code DRY. – Coreus Jul 11 '19 at 13:17
9

If you need a very minimalistic approach to this, I made one (originally forked from vuejs-tips to also support IE) which is available here: https://www.npmjs.com/package/v-debounce

Usage:

<input v-model.lazy="term" v-debounce="delay" placeholder="Search for something" />

Then in your component:

<script>
export default {
  name: 'example',
  data () {
    return {
      delay: 1000,
      term: '',
    }
  },
  watch: {
    term () {
      // Do something with search term after it debounced
      console.log(`Search term changed to ${this.term}`)
    }
  },
  directives: {
    debounce
  }
}
</script>
kissu
  • 40,416
  • 14
  • 65
  • 133
Coreus
  • 5,360
  • 3
  • 35
  • 50
  • 1
    Probably this one should be the accepted solution, with 100+ vote-ups. The OP asked for a compact solution like this, and it nicely decouples the debounce logic. – Barney Szabolcs May 19 '20 at 11:53
  • It will be so hard if you play with array, because this way is depends with the static data – Bcktr Sep 05 '21 at 01:38
7

Please note that I posted this answer before the accepted answer. It's not correct. It's just a step forward from the solution in the question. I have edited the accepted question to show both the author's implementation and the final implementation I had used.


Based on comments and the linked migration document, I've made a few changes to the code:

In template:

<input type="text" v-on:input="debounceInput" v-model="searchInput">

In script:

watch: {
  searchInput: function () {
    this.debounceInput();
  }
},

And the method that sets the filter key stays the same:

methods: {
  debounceInput: _.debounce(function () {
    this.filterKey = this.searchInput;
  }, 500)
}

This looks like there is one less call (just the v-model, and not the v-on:input).

tony19
  • 125,647
  • 18
  • 229
  • 307
MartinTeeVarga
  • 10,478
  • 12
  • 61
  • 98
  • Wouldn't this call `debounceInput()` twice for each change? `v-on:` will detect the input changes and call debounce, AND because the model is bound, searchInput's watch function will ALSO call `debounceInput`... right? – mix3d May 24 '17 at 17:58
  • @mix3d Do not consider this answer. It was just my investigation I did not want to put in the question. You are most likely right. Check the accepted answer. It's correct and I edited it to match the question. – MartinTeeVarga May 24 '17 at 21:44
  • My mistake... I didn't realize you had answered your own question, ha! – mix3d May 26 '17 at 11:38
3

In case you need to apply a dynamic delay with the lodash's debounce function:

props: {
  delay: String
},

data: () => ({
  search: null
}),

created () {
     this.valueChanged = debounce(function (event) {
      // Here you have access to `this`
      this.makeAPIrequest(event.target.value)
    }.bind(this), this.delay)

},

methods: {
  makeAPIrequest (newVal) {
    // ...
  }
}

And the template:

<template>
  //...

   <input type="text" v-model="search" @input="valueChanged" />

  //...
</template>

NOTE: in the example above I made an example of search input which can call the API with a custom delay which is provided in props

kissu
  • 40,416
  • 14
  • 65
  • 133
Roland
  • 24,554
  • 4
  • 99
  • 97
1

Although pretty much all answers here are already correct, if anyone is in search of a quick solution I have a directive for this. https://www.npmjs.com/package/vue-lazy-input

It applies to @input and v-model, supports custom components and DOM elements, debounce and throttle.

Vue.use(VueLazyInput)
  new Vue({
    el: '#app', 
    data() {
      return {
        val: 42
      }
    },
    methods:{
      onLazyInput(e){
        console.log(e.target.value)
      }
    }
  })
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://unpkg.com/lodash/lodash.min.js"></script><!-- dependency -->
<script src="https://unpkg.com/vue-lazy-input@latest"></script> 

<div id="app">
  <input type="range" v-model="val" @input="onLazyInput" v-lazy-input /> {{val}}
</div>
undefinederror
  • 821
  • 1
  • 8
  • 16
1

To create debounced methods you can use computeds, that way they won't be shared across multiple instances of your component:

<template>
  <input @input="handleInputDebounced">
<template>

<script>
import debounce from 'lodash.debouce';

export default {
  props: {
    timeout: {
      type: Number,
      default: 200,
    },
  },
  methods: {
    handleInput(event) {
      // input handling logic
    },
  },
  computed: {
    handleInputDebounced() {
      return debounce(this.handleInput, this.timeout);
    },
  },
}
</script>

You can make it work with uncontrolled v-model as well:

<template>
  <input v-model="debouncedModel">
<template>

<script>
import debounce from 'lodash.debouce';

export default {
  props: {
    value: String,
    timeout: {
      type: Number,
      default: 200,
    },
  },
  methods: {
    updateValue(value) {
      this.$emit('input', value);
    },
  },
  computed: {
    updateValueDebounced() {
      return debounce(this.updateValue, this.timeout);
    },
    debouncedModel: {
      get() { return this.value; },
      set(value) { this.updateValueDebounced(value); }
    },
  },
}
</script>
CyberAP
  • 1,195
  • 1
  • 11
  • 17
1

Here is a vue3 way

...
<input v-model="searchInput">
...
setup(){
  const searchInput = ref(null)
  const timeoutID = ref(null)

  watch(searchInput, (new, old) => {
      clearTimeout(timeoutID.value)
      timeoutID.value = setTimeout(() => {
         //Call function for searching
      }, 500) //millisecons before it is run
  })
  return {...}
}
Skywalker
  • 1,717
  • 1
  • 22
  • 25
0

If you are using Vue you can also use v.model.lazy instead of debounce but remember v.model.lazy will not always work as Vue limits it for custom components.

For custom components you should use :value along with @change.native

<b-input :value="data" @change.native="data = $event.target.value" ></b-input>

Amir Khadem
  • 681
  • 9
  • 26
0

1 Short version using arrow function, with default delay value

file: debounce.js in ex: ( import debounce from '../../utils/debounce' )

export default function (callback, delay=300) {
    let timeout = null
    return (...args) => {
        clearTimeout(timeout)
        const context = this
        timeout = setTimeout(() => callback.apply(context, args), delay)
    }
}

2 Mixin option

file: debounceMixin.js

export default {
  methods: {
    debounce(func, delay=300) {
      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");
      };
    }
  }
};

Use in vueComponent:

<script>
  import debounceMixin from "../mixins/debounceMixin";
  export default {
   mixins: [debounceMixin],
        data() {
            return {
                isUserIdValid: false,
            };
        },
        mounted() {
        this.isUserIdValid = this.debounce(this.checkUserIdValid, 1000);
        },
    methods: {
        isUserIdValid(id){
        // logic
        }
  }
</script>

another option, example

Vue search input debounce

devzom
  • 676
  • 1
  • 9
  • 19
0

Here's an example Vue 2 component that demonstrates how to use debounce.

<template>
  <div>
    <v-btn @click="properDebounceMyMethod">Proper debounce</v-btn>
    <v-btn @click="notWorkingDebounceMyMethod">!debounce</v-btn>
    <v-btn @click="myMethod">normal call</v-btn>
  </div>
</template>

<script lang="ts" >
import { defineComponent } from '@vue/composition-api';
import { debounce } from 'lodash';

export default defineComponent({
  name: 'DebounceExample',
  created() {
    // debounce instance method dynamically on created hook
    this.properDebounceMyMethod = debounce(this.properDebounceMyMethod, 500);
  },
  methods: {
    properDebounceMyMethod(){
      this.myMethod();
    },
    notWorkingDebounceMyMethod() {
      debounce(this.myMethod, 500);
    },
    myMethod() {
      console.log('hi from my method');
    },
  }
});
</script>
-1

I was able to use debounce with very little implementation.

I am using Vue 2.6.14 with boostrap-vue:

Add this pkg to your package.json: https://www.npmjs.com/package/debounce

Add this to main.js:

import { debounce } from "debounce";
Vue.use(debounce);

In my component I have this input:

          <b-form-input
            debounce="600"
            @update="search()"
            trim
            id="username"
            v-model="form.userName"
            type="text"
            placeholder="Enter username"
            required
          >
          </b-form-input>

All it does is call the search() method and the search method uses the form.userName for perform the search.

Fritz
  • 343
  • 1
  • 10
-1
<template>
  <input type="text" v-model="search" @input="debouncedSearch" />
</template>

<script>
import _ from 'lodash';

export default {
  data() {
    return {
      search: '',
    };
  },
  methods: {
    search() {
      // Perform the search here
      console.log(this.search);
    },
  },
  created() {
    this.debouncedSearch = _.debounce(this.search, 1000);
  },
};
</script>
Mak_091
  • 187
  • 1
  • 2
  • 14
-2

If you could move the execution of the debounce function into some class method you could use a decorator from the utils-decorators lib (npm install --save utils-decorators):

import {debounce} from 'utils-decorators';

class SomeService {

  @debounce(500)
  getData(params) {
  }
}
vlio20
  • 8,955
  • 18
  • 95
  • 180
-4
 public debChannel = debounce((key) => this.remoteMethodChannelName(key), 200)

vue-property-decorator

Mayxxp
  • 1
  • 3
    Could you please add more information about this solution? – nunop Jun 23 '20 at 11:07
  • 3
    Please elaborate a little bit more. Also, note that this is an old thread with well established answers, so can you clarify how your solution is more appropriate for the problem? – jpnadas Jun 23 '20 at 11:37
  • It helps more if you supply an explanation why this is the preferred solution and explain how it works. We want to educate, not just provide code. – the Tin Man Jun 24 '20 at 04:28