Vue cookbook · Beginner
Debounce user input with a custom ref
Expensive work (search requests, validation) runs on every keystroke.
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-modelon 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
refDebouncedanduseDebounceFnprovide 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
- Access child component methods with template refs and defineExpose
- Add SEO metadata and structured data in Nuxt
- Build an accessible modal with Teleport
- Build custom inputs with defineModel and modifiers
- Catch component errors with an error boundary
- Fetch data in a composable and cancel stale requests
- Fetch, refresh and share data with Nuxt useFetch
- Implement a dark mode toggle that respects system settings