useTemplateRef
A partir de Vue 3.5, la función useTemplateRef() es la forma recomendada para obtener referencias a elementos del DOM o a instancias de componentes hijos. Reemplaza al patrón anterior de crear un ref() con el mismo nombre que el atributo ref en la plantilla, proporcionando mayor claridad y mejor soporte de TypeScript.
Referencia a un elemento del DOM
<template>
<input ref="searchInput" type="text" placeholder="Buscar..." />
<button @click="focusInput">Enfocar</button>
</template>
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
const inputRef = useTemplateRef<HTMLInputElement>('searchInput')
onMounted(() => {
inputRef.value?.focus()
})
function focusInput() {
inputRef.value?.focus()
}
</script>
La función recibe como argumento el valor del atributo ref en la plantilla ('searchInput'). El genérico <HTMLInputElement> proporciona tipado completo del elemento, habilitando el autocompletado de propiedades y métodos como .focus(), .value, .selectionStart, etc.
Referencia a un componente hijo
También se puede usar useTemplateRef para acceder a la instancia expuesta de un componente hijo:
<template>
<ChildComponent ref="child" />
<button @click="resetChild">Reset hijo</button>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childRef = useTemplateRef<InstanceType<typeof ChildComponent>>('child')
function resetChild() {
childRef.value?.reset()
}
</script>
El componente hijo debe exponer los métodos que el padre quiera usar mediante defineExpose:
<!-- ChildComponent.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
function reset() {
count.value = 0
}
defineExpose({ reset })
</script>
useId
La función useId(), disponible desde Vue 3.5, genera un identificador único por instancia de componente. Es especialmente útil para asociar etiquetas <label> con sus inputs mediante el atributo for/id, y para atributos de accesibilidad ARIA, asegurando que cada id sea único incluso cuando el componente se usa múltiples veces.
<template>
<div class="form-field">
<label :for="fieldId">{{ label }}</label>
<input :id="fieldId" v-model="model" :type="type" :aria-describedby="hintId" />
<span v-if="hint" :id="hintId" class="hint">{{ hint }}</span>
</div>
</template>
<script setup lang="ts">
import { useId } from 'vue'
const model = defineModel<string>({ required: true })
defineProps<{
label: string
type?: string
hint?: string
}>()
const fieldId = useId()
const hintId = useId()
</script>
Cada llamada a useId() genera un id único como v-0, v-1, etc. Si la aplicación usa SSR (Server Side Rendering), los ids generados en el servidor coinciden con los del cliente, evitando desajustes de hidratación.
Uso desde el padre con múltiples instancias:
<template>
<FormField v-model="nombre" label="Nombre" hint="Tu nombre completo" />
<FormField v-model="email" label="Email" type="email" hint="Un email válido" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import FormField from './FormField.vue'
const nombre = ref('')
const email = ref('')
</script>
Cada instancia de FormField tendrá ids únicos, evitando conflictos.
onWatcherCleanup
La función onWatcherCleanup(), introducida en Vue 3.5, permite registrar una función de limpieza dentro de un watcher. Esta función se ejecuta justo antes de que el efecto del watcher vuelva a ejecutarse o cuando el watcher se detiene. Es ideal para cancelar peticiones fetch, limpiar timers o liberar recursos.
Cancelar fetch con AbortController
<template>
<input v-model="query" placeholder="Buscar..." />
<ul v-if="results.length">
<li v-for="item in results" :key="item.id">{{ item.name }}</li>
</ul>
<p v-if="loading">Buscando...</p>
</template>
<script setup lang="ts">
import { ref, watch, onWatcherCleanup } from 'vue'
const query = ref('')
const results = ref<any[]>([])
const loading = ref(false)
watch(query, async (newQuery) => {
if (newQuery.length < 2) {
results.value = []
return
}
const controller = new AbortController()
onWatcherCleanup(() => {
controller.abort()
})
loading.value = true
try {
const response = await fetch(`/api/search?q=${newQuery}`, {
signal: controller.signal
})
results.value = await response.json()
} catch (error) {
if ((error as Error).name !== 'AbortError') {
console.error('Error en búsqueda:', error)
}
} finally {
loading.value = false
}
})
</script>
Cada vez que query cambia, la petición anterior se cancela automáticamente mediante el AbortController, evitando condiciones de carrera donde una respuesta lenta podría sobrescribir resultados más recientes.
Limpiar timers
<script setup lang="ts">
import { ref, watch, onWatcherCleanup } from 'vue'
const inputValue = ref('')
const debouncedValue = ref('')
watch(inputValue, (newVal) => {
const timer = setTimeout(() => {
debouncedValue.value = newVal
}, 300)
onWatcherCleanup(() => {
clearTimeout(timer)
})
})
</script>
Watch con opción once
A partir de Vue 3.4, se puede crear un watcher que se ejecute una sola vez y se detenga automáticamente. Esto es útil para realizar una acción la primera vez que un valor cambia:
<script setup lang="ts">
import { ref, watch } from 'vue'
const searchTerm = ref('')
watch(
searchTerm,
(newValue) => {
console.log('Primera búsqueda realizada:', newValue)
// Este callback solo se ejecuta una vez
},
{ once: true }
)
</script>
Tras la primera ejecución, el watcher se elimina automáticamente sin necesidad de llamar a stop() manualmente.
Watch con deep numérico
Desde Vue 3.5, la opción deep acepta un valor numérico que indica la profundidad máxima de observación. Esto mejora el rendimiento al evitar la observación profunda completa en objetos grandes:
<script setup lang="ts">
import { ref, watch } from 'vue'
const config = ref({
theme: {
colors: {
primary: '#007bff',
secondary: '#6c757d'
},
fonts: {
heading: 'Arial',
body: 'Helvetica'
}
},
settings: {
darkMode: false
}
})
// Observa cambios hasta 2 niveles de profundidad
// Detecta cambios en config.theme y config.settings
// No detecta cambios en config.theme.colors.primary
watch(
config,
(newConfig) => {
console.log('Config actualizado (nivel 2):', newConfig)
},
{ deep: 2 }
)
</script>
Con deep: 1 se observan solo los cambios en las propiedades directas del objeto. Con deep: 2, se observan también las propiedades del segundo nivel.
Pausar y reanudar watchers
Desde Vue 3.5, el objeto WatchHandle devuelto por watch y watchEffect incluye métodos para pausar, reanudar y detener el efecto:
<template>
<div>
<input v-model="searchTerm" placeholder="Buscar..." />
<p>Búsquedas realizadas: {{ searchCount }}</p>
<button @click="toggleWatcher">
{{ isPaused ? 'Reanudar' : 'Pausar' }} observador
</button>
<button @click="stopWatcher">Detener definitivamente</button>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
const searchTerm = ref('')
const searchCount = ref(0)
const isPaused = ref(false)
const { pause, resume, stop } = watch(searchTerm, () => {
searchCount.value++
console.log(`Busqueda #${searchCount.value}: ${searchTerm.value}`)
})
function toggleWatcher() {
if (isPaused.value) {
resume()
} else {
pause()
}
isPaused.value = !isPaused.value
}
function stopWatcher() {
stop()
console.log('Observador detenido permanentemente')
}
</script>
Los metodos disponibles en el WatchHandle son:
pause(): suspende temporalmente el watcher. Los cambios en las dependencias no disparan el callback.resume(): reanuda el watcher pausado. Si hubo cambios mientras estaba pausado, el callback se ejecuta inmediatamente.stop(): detiene el watcher de forma permanente.
shallowRef y shallowReactive
Cuando se trabaja con objetos grandes o con datos que no necesitan reactividad profunda, las versiones "shallow" evitan la conversion recursiva de propiedades:
shallowRef
shallowRef solo dispara actualizaciones cuando se reemplaza el .value completo, no cuando se modifican propiedades internas:
flowchart TB
M1["Mutar propiedades internas"] --> N1["No dispara actualización"]
M2["Reemplazar todo el .value"] --> S1["Sí dispara actualización"]
<script setup lang="ts">
import { shallowRef } from 'vue'
const state = shallowRef({ count: 0, data: [1, 2, 3] })
// NO dispara actualizacion (mutacion interna)
state.value.count = 5
// SI dispara actualizacion (reemplazo completo)
state.value = { count: 5, data: [1, 2, 3] }
</script>
shallowReactive
shallowReactive solo hace reactivas las propiedades del primer nivel. Las propiedades anidadas son objetos planos sin reactividad:
<script setup lang="ts">
import { shallowReactive } from 'vue'
const state = shallowReactive({
nombre: 'Ana',
direccion: { calle: 'Gran Vía', numero: 10 }
})
// SI dispara actualizacion (propiedad de primer nivel)
state.nombre = 'Carlos'
// NO dispara actualizacion (propiedad anidada)
state.direccion.calle = 'Diagonal'
</script>
Estos tipos son utiles cuando se almacenan datos de solo lectura, resultados de APIs que no se modifican, o integraciones con librerias externas.
triggerRef
Cuando se usa shallowRef y se realizan mutaciones internas, se puede forzar una actualizacion manualmente con triggerRef:
<script setup lang="ts">
import { shallowRef, triggerRef } from 'vue'
const lista = shallowRef([1, 2, 3])
function agregarElemento() {
lista.value.push(4) // mutacion interna: no dispara actualizacion
triggerRef(lista) // fuerza la actualizacion manualmente
}
</script>
toValue
La función toValue() es una utilidad que desenvuelve de forma segura un valor que puede ser un ref, un getter (función) o un valor plano:
<script setup lang="ts">
import { ref, toValue } from 'vue'
const countRef = ref(10)
const countGetter = () => 20
const countPlain = 30
console.log(toValue(countRef)) // 10
console.log(toValue(countGetter)) // 20
console.log(toValue(countPlain)) // 30
</script>
toValue es especialmente útil dentro de composables, donde un parámetro puede ser cualquiera de estos tres tipos y se necesita obtener el valor actual sin preocuparse del tipo de envoltorio:
import { toValue, watch } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
export function useDebounce(source: MaybeRefOrGetter<string>, delay = 300) {
const debounced = ref(toValue(source))
watch(
() => toValue(source),
(newVal) => {
const timer = setTimeout(() => {
debounced.value = newVal
}, delay)
onWatcherCleanup(() => clearTimeout(timer))
}
)
return debounced
}
Esta función acepta un ref, una función getter o un string directamente, haciéndola extremadamente flexible para el consumidor.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Vuejs es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Vuejs
Explora más contenido relacionado con Vuejs y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje