How to create a debounced ref in Vue 3 using Composition API

By Thomas Findlay 5 Apr, 2021

With the introduction of Composition API, we got a new way of writing reactive logic, namely, ref and reactive methods. In this article, I want to show you how you can create a debounced ref that will update its value only after a specified delay. A denounced ref can be very useful, for example, if you have an input field with an autocomplete where an API request is made after the search query state changes.

Debounced search query

Debouncing is a nice optimisation pattern, and without it, an API request would be made after every keystroke. Battering a server is not optimal, so let's get started.

Project Setup

To showcase this example, I have created a new project with Vite. If you would like to follow along, you can create one by running yarn create @vitejs/app debounced-ref --template vue or npm init @vitejs/app debounced-ref --template vue. You can also find the full code example in this GitHub repo.

App.vue

As I mentioned at the start, a debounced ref could be used to delay API requests when a user enters search criteria in an input field. Below you can see the setup for it.

<template>
  <div>
    <label :class="$style.label">Search query</label>
    <input :class="$style.input" v-model="query" type="text" />
    <div>Value: {{ query }}</div>
  </div>
</template>

<script>
import { watch } from 'vue'
import useDebouncedRef from './composables/useDebouncedRef'
export default {
  setup() {
    const query = useDebouncedRef('', 400)

    watch(query, newQuery => {
      console.log({ newQuery })
      // init an API request
    })

    return {
      query,
    }
  },
}
</script>

<style module>
.label {
  display: block;
}

.input {
  margin-top: 5px;
  margin-bottom: 20px;
}
</style>

In the template, we have a label, input, and div that contains the query value. When we're done, you will see that the value is updated only after a delay. In the setup method, we create a debounced ref using useDebouncedRef function and pass an empty string as an initial value as well as 400, which is the debounce delay. We will create it in a moment. Besides that, we also have a watcher that observes the query ref. That's where you could initialise a function that would perform an API request.

useDebouncedRef.js

Here is the implementation of the useDebouncedRef composable.

import { ref, customRef } from 'vue'

const debounce = (fn, delay = 0, immediate = false) => {
  let timeout
  return (...args) => {
    if (immediate && !timeout) fn(...args)
    clearTimeout(timeout)

    timeout = setTimeout(() => {
      fn(...args)
    }, delay)
  }
}

const useDebouncedRef = (initialValue, delay, immediate) => {
  const state = ref(initialValue)
  const debouncedRef = customRef((track, trigger) => ({
    get() {
      track()
      return state.value
    },
    set: debounce(
      value => {
        state.value = value
        trigger()
      },
      delay,
      immediate
    ),
  }))
  return debouncedRef
}

export default useDebouncedRef

In the useDebouncedRef.js file, we have debounce and useDebouncedRef functions. The debounce function takes care of executing a callback function after the specified delay time passed. Besides a callback function and delay number, it also accepts a third parameter called immediate. As the name suggests, it is used to indicate if the callback should be executed immediately. This little helper function could be abstracted into its own utility file and reused in other parts of your applications.

In the useDebouncedRef composable, we declare a new reactive value using the ref method imported from the vue package. However, we also use a customRef, and that's where the magic happens. The customRef gives us explicit control over tracking dependencies and triggering state updates. The getter calls the track method and returns the current state value. For the setter, we assign a result of the debounce function, which receives a callback as the first parameter and forwards delay and immediate arguments. In the callback, we update the state ref and trigger the update of the customRef. That's all. If you try to enter something in the input field, you should see, that the value text updates only after you stop typing for a specified amount of time.

I hope you enjoyed this article. If you would like to learn more tips, advanced patterns, techniques and best practices related to Vue, you might want to check out "Vue - The Road To Enterprise" book, sign up for the newsletter, and follow me on Twitter.


Want to learn something new?

Subscribe to the newsletter!


Thomas Findlay photo

About author

Thomas Findlay is a 5 star rated mentor, full-stack developer, consultant, and technical writer. He works with many different technologies such as JavaScript, Vue, React, React Native, Node.js, Python, PHP, and more. He has obtained MSc in Advanced Computer Science degree with Distinction at Exeter University, as well as First-Class BSc in Web Design & Development at Northumbria University.

Over the years, Thomas has worked with many developers and teams from beginners to advanced and helped them build and scale their applications and products. He also mentored a lot of developers and students, and helped them progress in their careers.

To get to know more about Thomas you can check out his Codementor and Twitter profiles.