Vue cookbook · Beginner

Debounce user input with a custom ref

Expensive work (search requests, validation) runs on every keystroke.

Create a free account to save this page, take notes and track your progress. Sign in

Approach

Create a debounced ref with customRef: reads are tracked normally, writes update the value only after the user stops typing for a moment.

Solution

// composables/useDebouncedRef.ts
import { customRef } from "vue";

export function useDebouncedRef<T>(initial: T, delay = 300) {
  let value = initial;
  let timer: ReturnType<typeof setTimeout> | undefined;
  return customRef<T>((track, trigger) => ({
    get() {
      track();
      return value;
    },
    set(next) {
      clearTimeout(timer);
      timer = setTimeout(() => {
        value = next;
        trigger();
      }, delay);
    },
  }));
}
<script setup lang="ts">
import { watch } from "vue";
import { useDebouncedRef } from "./composables/useDebouncedRef";

const search = useDebouncedRef("", 400);
watch(search, (q) => console.log("search for", q));      // runs 400 ms after typing stops
</script>

<template>
  <input :value="search" @input="search = ($event.target as HTMLInputElement).value" />
</template>

Pitfalls

  • Debouncing the displayed value with v-model on the debounced ref, making the input feel laggy; debounce the work (or keep a separate immediate ref for the input).
  • Forgetting to clear timers on unmount for long delays.
  • Debouncing actions that must be immediate, like form submission.

Variations

  • VueUse's refDebounced and useDebounceFn provide the same behaviour.
  • Throttle instead of debounce for continuous events like scrolling.

Further reading: customRef

Reference: https://vuejs.org/api/reactivity-advanced.html#customref

More Vue recipes

Was this page helpful?