Telerik blogs

Using stateful functions on the methods block of the options API in Vue doesn’t work how you might expect.

A common pitfall when developing Vue apps, and one of the hardest ones to pinpoint and debug its issues, is using stateful functions on the methods block of the options API.

Background

Let’s start with a little background. A stateful function is a function that retains certain state between executions. In most cases, these functions are higher order functions—functions that return a function.

A great example of a higher order stateful function, and one that is the culprit in many of these scenarios, is Lodash’s debounce. It’s a super handy utility function that takes a function as its main parameter and returns a debounced function.

To understand what debouncing means, as explained by Lodash’s docs: “delays invoking [a function] until after [certain] milliseconds have elapsed since the last time the debounced function was invoked.” Or, in simpler terms, it prevents a function from being called repeatedly until a certain amount of time has passed.

The Example Component

In order to better understand the problem first we have to create a few components and a sample application to demonstrate the issue.

Reusable.vue

<template>
  <p>My amazing counter</p>
  <button @click="addToCounterDebounced">+</button>
  State: {{ counter }}
</template>

<script>
import { debounce } from "lodash";

export default {
  data: () => ({
    counter: 0,
  }),
  methods: {
    addToCounter() {
      this.counter++;
    },
    addToCounterDebounced: debounce(function() {
      this.counter++;
    }, 2000),
  },
};
</script>

Let’s dive into it.

  1. We create a button that calls an addToCounterDebounce function when clicked.
  2. The addToCounterDebounce function is declared within the methods block of the options API, and calls debounce to generate the debounced function.
  3. Within the debounce call we are creating an anonymous function that increases the component’s counter property by 1.
  4. We set a 2000 millisecond (2 seconds) debounce timer for this function, which means that if this function is called multiple times within 2 seconds of the last call—it will only be executed once. Remember this!
  5. Finally, we print the counter under the button so we can easily see it execute.

The component after clicking it once:

My amazing counter has a plus button and a field State: 1

Even if we click the button 100 times, the state will only increase by once every 2 seconds that there is no function being called, this is the nature of debounced methods.

If you were wondering about debounce and when to use it, a good use cases for debounced functions in real world scenarios are to delay user’s input from making API calls.

For example, an email input field that pings the server to check if the email is already in use. You probably don’t want to ping the server for every single keystroke or change, and rather want to wait until the user is done typing to fire the method.

The Problem

If you were to mount this component in an app and use it by itself, you probably would see absolutely no problem. You could even write some unit tests and everything would work perfectly.

But what would happen when two instances of the same component are created?

App.vue

<template>
  <div>
    <Reusable />
    <Reusable />
  </div>
</template>
<script>

import Reusable from "./Reusable.vue";

export default {
  components: { Reusable }
};
</script>

Notice that we have imported the Reusable component we created earlier into our app, and created two different instances of it on lines 3 and 4. Everything should work the same, right? Right?

Go ahead and give it a shot. Click on both state + buttons a few times within the two second window. We would expect both states to get a +1.

User clicks the first button several times and then the second button several times. After a beat, the second one increases from 0 to 1

Hmmm… No matter how many times we click on both components, only the one where we clicked last will get updated. But why?

Why Doesn’t It Work?

As we learned in the beginning of the article, debounce is a stateful function—it keeps internal state while the execution of the component is in progress. When we start clicking around, the function receives the reference to the clicked component’s counter property.

However, when we click on the second button, the counter reference is replaced by the second component’s counter. The first reference is “forgotten,” as the function is awaiting the timer to complete to execute its internal call of this.counter++.

The Solution

Thankfully, the solution is straightforward and also a good practice to keep for any use of stateful functions and higher order functions within Vue components.

Reusable.vue

<template>
  <p>My amazing counter</p>
  <button @click="addToCounter">+</button>
  State: {{ counter }}
</template>

<script>
import { debounce } from "lodash";

export default {
  data: () => ({
    counter: 0,
  }),
  mounted() {
    this.debouncedAddToCounter = debounce(function () {
      this.counter++;
    }, 2000);
  },
  methods: {
    addToCounter() {
      this.counter++;
    },
    addToCounter() {
      this.debouncedAddToCounter();
    },
  },
};
</script>

Let’s take a look.

  1. In the mounted block, we assign the result of our debounce call to a debounceAddToCounter as a non-reactive property. Now the debounced function will only work within the scope of each instance.
  2. We rename the addToCounterDebounced to addToCounter for clarity, and replace it on the button’s click listener.
  3. Within addToCounter we can now call this.debouncedAddToCounter.

Go ahead and give this a shot, and click away at both buttons.

Both buttons update to 1 after a beat

Now both of our counters update separately as intended.

Wrapping Up

This pitfall is more common that it should be, so hopefully after reading this article you will be ready and on the lookout for possible nasty little bugs created by misuse of stateful functions in Vue components.

Happy coding!


Vue
About the Author

Marina Mosti

Marina Mosti is a frontend web developer with over 18 years of experience in the field. She enjoys mentoring other women on JavaScript and her favorite framework, Vue, as well as writing articles and tutorials for the community. In her spare time, she enjoys playing bass, drums and video games.

Related Posts

Comments

Comments are disabled in preview mode.