Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Debouncing: Data can Hold Anything

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

The only problem is that we made our search too awesome. When I type... wow! Look at those are Ajax requests - one for every character I type. It's, sort of unnecessarily flooding our API.

This is a common problem with a common solution: debouncing, which is almost as fun as it sounds. With debouncing, instead of sending an Ajax request after every letter, we wait until the user stops typing - maybe 200 milliseconds - and then make the request.

The cool thing is that we're going to be able to add debouncing entirely inside of the search-bar component. This means that our catalog code won't need to change at all; it will instantly be able to take advantage of it.

How can we do that? By delaying the custom search-products event until the user is done typing.

Adding the setTimeout()

Start by adding setTimeout(), passing it an arrow function, then moving the $emit call inside. Set the timeout to 200 milliseconds. Now, instead of emitting the event immediately, it will be slightly delayed.

... lines 1 - 13
export default {
... lines 15 - 20
methods: {
onInput() {
setTimeout(() => {
this.$emit('search-products', { term: this.searchTerm });
}, 200);
... lines 29 - 30

Easy peasy! Oh, and the arrow function is important: if we used a traditional function, the this variable wouldn't be our Vue instance. Silly JavaScript!

Now, some of you probably realize that this isn't going to quite work yet. If we refresh... and then type really fast. Ah! It still sent four Ajax requests... it just waited 200 milliseconds before making each of them. Whoops!

Storing and Clearing the Timeout

To get debouncing to work, what we really need to do is, onInput(), if a timeout is currently active and waiting to be called, we need to cancel it and restart the timer.

To do that, we need to keep track of the return value of setTimeout(): it's a "timeout id". Then, the next time onInput() is executed we will call clearTimeout() and pass it that id.

None of this is too complex... but I do have one question: where should we store this "timeout id" so that we can reference it later? The easiest thing to do is to store it as data.

Add a new data called searchTimeout set to null.

... lines 1 - 13
export default {
... line 15
data() {
return {
... line 18
searchTimeout: null,
... lines 22 - 33
... lines 35 - 36

Then, in the function, say this.searchTimeout = setTimeout(). Now that this is stored, at the top of the function, check if it has a value: if this.searchTimeout, then clearTimeout(this.searchTimeout).

... lines 1 - 21
methods: {
onInput() {
if (this.searchTimeout) {
... lines 27 - 31
... lines 34 - 36

Oh, and to round this all out nicely, once the callback is finally called, we can reset the searchTimeout back to null.

... lines 1 - 22
onInput() {
... lines 24 - 27
this.searchTimeout = setTimeout(() => {
this.$emit('search-products', { term: this.searchTerm });
this.searchTimeout = null;
}, 200);
... lines 33 - 36

Now, if we type really fast, the second time onInput() is called, it will clear the timeout, and then, below, start a new one.

Let's try it! I'll refresh to be sure then... type super fast. Yes! Just one Ajax call down here to the categories API. That's beautiful!

Non-Reactive Stuff on Data?

For me, the most interesting thing about what we just did is that, in some ways, storing searchTimeout as data seems like an abuse of data. Normally we put something in data because we want it to be reactive: when that value changes, we want any component that uses it to re-render. For searchTimeout... we don't really need that! We just needed a place to stash that value. But... this is fine. The main purpose of data is to store "reactive" data. But if you need a place to store something else, go nuts.

Next, if we have some business logic that we want to re-use between components, where should that live? Let's take our organization up to the next level!

Leave a comment!

Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

This course is also built to work with Vue 3!

What JavaScript libraries does this tutorial use?

// package.json
    "devDependencies": {
        "@symfony/webpack-encore": "^0.30.0", // 0.30.2
        "axios": "^0.19.2", // 0.19.2
        "bootstrap": "^4.4.1", // 4.5.0
        "core-js": "^3.0.0", // 3.6.5
        "eslint": "^6.7.2", // 6.8.0
        "eslint-config-airbnb-base": "^14.0.0", // 14.1.0
        "eslint-plugin-import": "^2.19.1", // 2.20.2
        "eslint-plugin-vue": "^6.0.1", // 6.2.2
        "regenerator-runtime": "^0.13.2", // 0.13.5
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^8.0.0", // 8.0.2
        "vue": "^2.6.11", // 2.6.11
        "vue-loader": "^15.9.1", // 15.9.2
        "vue-template-compiler": "^2.6.11", // 2.6.11
        "webpack-notifier": "^1.6.0" // 1.8.0