This content originally appeared on Telerik Blogs and was authored by Marina Mosti
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.
- We create a button that calls an
addToCounterDebounce
function when clicked. - The
addToCounterDebounce
function is declared within themethods
block of the options API, and callsdebounce
to generate the debounced function. - Within the
debounce
call we are creating an anonymous function that increases the component’scounter
property by 1. - 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! - Finally, we print the
counter
under the button so we can easily see it execute.
The component after clicking it once:
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.
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.
- In the
mounted
block, we assign the result of ourdebounce
call to adebounceAddToCounter
as a non-reactive property. Now the debounced function will only work within the scope of each instance. - We rename the
addToCounterDebounced
toaddToCounter
for clarity, and replace it on the button’sclick
listener. - Within
addToCounter
we can now callthis.debouncedAddToCounter
.
Go ahead and give this a shot, and click away at both buttons.
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!
This content originally appeared on Telerik Blogs and was authored by Marina Mosti
Marina Mosti | Sciencx (2024-08-26T08:52:25+00:00) Why You Should Not Use Stateful Functions as Methods in Options API for Vue. Retrieved from https://www.scien.cx/2024/08/26/why-you-should-not-use-stateful-functions-as-methods-in-options-api-for-vue/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.