Control how often reactive updates propagate for performance and UX.
Debounced Ref
Delay updates until input stops:
typescriptimport { ref, watch, type Ref } from 'vue' export function useDebouncedRef<T>(value: T, delay = 300): Ref<T> { const debouncedValue = ref(value) as Ref<T> let timeout: ReturnType<typeof setTimeout> watch( () => value, (newValue) => { clearTimeout(timeout) timeout = setTimeout(() => { debouncedValue.value = newValue }, delay) } ) return debouncedValue } // Usage const searchInput = ref('') const debouncedSearch = useDebouncedRef(searchInput.value, 500)
Custom Debounced Ref with customRef
typescriptimport { customRef } from 'vue' export function useDebouncedRef<T>(initialValue: T, delay = 300) { let timeout: ReturnType<typeof setTimeout> let value = initialValue return customRef((track, trigger) => ({ get() { track() return value }, set(newValue: T) { clearTimeout(timeout) timeout = setTimeout(() => { value = newValue trigger() }, delay) } })) }
Throttled Ref
Limit updates to a maximum frequency:
typescriptimport { customRef } from 'vue' export function useThrottledRef<T>(initialValue: T, interval = 100) { let value = initialValue let lastUpdate = 0 let pendingValue: T | null = null let timeout: ReturnType<typeof setTimeout> | null = null return customRef((track, trigger) => ({ get() { track() return value }, set(newValue: T) { const now = Date.now() if (now - lastUpdate >= interval) { value = newValue lastUpdate = now trigger() } else { pendingValue = newValue if (!timeout) { timeout = setTimeout(() => { if (pendingValue !== null) { value = pendingValue pendingValue = null lastUpdate = Date.now() trigger() } timeout = null }, interval - (now - lastUpdate)) } } } })) }
Practical Example: Search Input
vue<script setup> import { ref, watch } from 'vue' import { useDebouncedRef } from '@/composables/useDebouncedRef' const searchQuery = useDebouncedRef('', 400) const results = ref([]) const loading = ref(false) watch(searchQuery, async (query) => { if (!query) { results.value = [] return } loading.value = true try { const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`) results.value = await response.json() } finally { loading.value = false } }) </script> <template> <input v-model="searchQuery" placeholder="Search..." /> <p v-if="loading">Searching...</p> <ul> <li v-for="result in results" :key="result.id">{{ result.name }}</li> </ul> </template>