VueJS 2 debounce on multiple components
Asked Answered
L

2

15

I have a Vue component that uses multiple sub-components on it. And on those sub-components I have a watcher that watches for data changes and processes those changes. I would like to implement debounce for this.

    watch: {
    data: {
      handler: function () {
        this.processData()
      },
      deep: true
    }
  },
  methods: {
    processData: debounce(function () {
      console.log(this.id)
    }, 250),

The problem is that debounce works so it executes only on the last sub-component.

I have found a solution for debounce function that accepts an additional id debounceWithId

However there problem is that if I specify this function as follows:

  methods: {
    processData: debounceWithId(function () {
      console.log(this.id)
    }, 250, this.id),

the last this.id is undefined.

What would be a correct way of using debounce in multiple components so the function fires separately on each component?

Lamson answered 27/8, 2017 at 20:2 Comment(0)
I
29

First let me add an example that replicates the issue you are describing.

console.clear()

function debounce(func, wait, immediate) {
    var timeout;
    return function() {
        console.log("Called from component ", this._uid)
        var context = this, args = arguments;
        var later = function() {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
    };
};

Vue.component("doesntwork",{
  props:["value"],
  template:`<div>Component #{{_uid}} Value: {{innerValue}}</div>`,
  data(){
    return {
      innerValue: this.value
    }
  },
  watch:{
    value(newVal){
      this.processData(newVal)
    }
  },
  methods:{
    processData: debounce(function(newVal){
      this.innerValue = newVal
    }, 1000)
  },
})


new Vue({
  el: "#app",
  data:{
    parentValue: null,
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://unpkg.com/[email protected]"></script>
<div id="app">
  Type some text. Wait one second. Only the *last* component is updated.<br>
  <input type="text" v-model="parentValue">
  <doesntwork :value="parentValue"></doesntwork>
  <doesntwork :value="parentValue"></doesntwork>
  <doesntwork :value="parentValue"></doesntwork>
</div>

Essentially what is going on here is that the debounced function is created when the component is compiled, and each instance of the component shares the same debounced function. The context of this will be different in each one, but it is the same function. I added a console.log in the generated debounced function so that you can see that all three components are sharing the same function. That being the case, the function is doing what it is designed to do; it executes once after the elapsed period of time, which is why only the last component is updated.

To get around that behavior you need a unique debounce function for each component. Here are two ways you can do that.

Method One

You can initialize your processData method with what amounts to a placeholder.

methods: {
  processData(){}
}

Then, in the created lifecycle event, change the processData method to the debounced method.

created(){
  this.processData = debounce(function(){
    console.log(this.id)
  }, 250)
}

This will give each component a unique debounced function and should take care of the issue where only the last component works properly.

Here is an example modified from the above example.

console.clear()

Vue.component("works",{
  props:["value"],
  template:`<div>Component #{{_uid}} Value: {{innerValue}}</div>`,
  data(){
    return {
      innerValue: this.value,
    }
  },
  watch:{
    value(newVal){
      this.processData(newVal)
    }
  },
  methods:{
    processData() {}
  },
  created(){
    this.processData = _.debounce(function(newVal){
      this.innerValue = newVal
    }, 1000)
  }
})

new Vue({
  el: "#app",
  data:{
    parentValue: null,
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://unpkg.com/[email protected]"></script>
<div id="app">
  Type some text. Wait one second. <em>All</em> components are updated.<br>
  <input type="text" v-model="parentValue">
  <works :value="parentValue"></works>
  <works :value="parentValue"></works>
  <works :value="parentValue"></works>
</div>

Method Two

Thanks to @RoyJ for suggesting this. You can define the processData method in data. Typically you do not do this because you don't often want multiple copies of a function, and that's why the methods section of a component definition exists, but in a case, like this where you need a unique function for each component, you can define the method in the data function because the data function is called for every instance of the component.

data(){
  return {
    innerValue: this.value,
    processData: _.debounce(function(newVal){
      this.innerValue = newVal
    }, 1000)
  }
},

Here is an example using that approach.

console.clear()

Vue.component("works",{
  props:["value"],
  template:`<div>Component #{{_uid}} Value: {{innerValue}}</div>`,
  data(){
    return {
      innerValue: this.value,
      processData: _.debounce(function(newVal){
        this.innerValue = newVal
      }, 1000)
    }
  },
  watch:{
    value(newVal){
      this.processData(newVal)
    }
  },
})

new Vue({
  el: "#app",
  data:{
    parentValue: null,
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://unpkg.com/[email protected]"></script>
<div id="app">
  Type some text. Wait one second. <em>All</em> components are updated.<br>
  <input type="text" v-model="parentValue">
  <works :value="parentValue"></works>
  <works :value="parentValue"></works>
  <works :value="parentValue"></works>
</div>
Iron answered 27/8, 2017 at 22:38 Comment(6)
@RoyJ What's your thought? In the codepen I'm not really returning a value, just updating a data property. In the non-working component, the debounced method only ever even fires once. Which...gives me a bit of an aha moment. All of the components are using the same function definition, the point of which is to fire once after the period is over.Iron
For what it's worth, computeds work the same way: if you put debounce in the setter, it will be one debounce for all instances. This is a somewhat subtle unexpected behavior, but it should be rare if your code is DRY. In the example, we're making multiple copies of data; the debounce should probably be done in the parent and be passed as the prop.Hither
Also FWIW, you can define processData in the data section, where it will be created once for each instance.Hither
@RoyJ I expect anything (outside the data function) using a debounced will suffer the same issue. That's a great thought about the data function by the way; it really is a factory function in line with what you were thinking last night. It's non-intuitive, but I agree, it should solve the problem as well.Iron
Thank you - now it works as I expected. I am using method two as it looks cleaner and adds fewer lines to the code.Juetta
You saved me hours of debugging.Preparator
G
1

another solution that works for me is using $watch api to add a watcher after the component is created then the debounce function will not be shared between components

created () {
  this.$watch(
    'foo',
    debounce(function bar() {
      // do something
    },
    {}
  )
}
Gerrald answered 14/8, 2020 at 6:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.