En esta página
computed() y watchers — estado derivado y efectos
computed() — estado derivado cacheado
computed() crea un valor reactivo que se deriva de otros datos reactivos. Vue rastrea automáticamente las dependencias y recalcula el valor solo cuando alguna de ellas cambia.
import { ref, computed } from 'vue'
const temperatura = ref(25) // Celsius
// Se recalcula automáticamente cuando temperatura.value cambia
const fahrenheit = computed(() => temperatura.value * 9/5 + 32)
const kelvin = computed(() => temperatura.value + 273.15)
console.log(fahrenheit.value) // 77
temperatura.value = 30
console.log(fahrenheit.value) // 86 — recalculado automáticamentePor qué usar computed en lugar de una función
Ambos enfoques parecen equivalentes, pero tienen una diferencia crucial:
<script setup lang="ts">
const lista = ref([1, 2, 3, 4, 5])
// ❌ Función — se ejecuta en CADA rerenderizado
function totalFuncion() {
console.log('Recalculando...')
return lista.value.reduce((a, b) => a + b, 0)
}
// ✅ computed — se ejecuta solo cuando lista.value cambia
const totalComputed = computed(() => {
console.log('Recalculando...')
return lista.value.reduce((a, b) => a + b, 0)
})
</script>
<template>
<!-- totalFuncion() se llama en cada rerenderizado aunque lista no cambie -->
<!-- totalComputed se usa desde caché si lista no cambió -->
<p>{{ totalFuncion() }} vs {{ totalComputed }}</p>
</template>Si el componente rerenderiza por cualquier razón (un prop no relacionado cambió, el padre rerenderizó), totalFuncion() se ejecuta de nuevo mientras que totalComputed devuelve el valor cacheado.
Computed con TypeScript
Vue infiere el tipo de retorno automáticamente, pero puedes ser explícito:
// Inferencia automática — Computed<number>
const doble = computed(() => contador.value * 2)
// Tipo explícito cuando TypeScript necesita ayuda
const elemento = computed<HTMLElement | null>(() =>
document.getElementById('mi-elemento')
)Computed de escritura (writable computed)
Por defecto, los computed son de solo lectura. Puedes hacerlos de escritura pasando un objeto con get y set:
const grados = ref(0)
const radianes = computed({
get: () => grados.value * (Math.PI / 180),
set: (rad: number) => {
grados.value = rad * (180 / Math.PI)
}
})
// Ahora puedes escribir también
radianes.value = Math.PI / 2 // grados.value se convierte en 90Los computed de escritura son especialmente útiles para inputs enlazados con v-model que necesitan transformar datos.
watch() — observar cambios específicos
watch() ejecuta una función callback cuando una fuente específica cambia. A diferencia de watchEffect, es lazy por defecto (no se ejecuta al inicio) y recibe el valor nuevo y el anterior:
import { ref, watch } from 'vue'
const contador = ref(0)
// Fuente simple — ref
watch(contador, (nuevo, anterior) => {
console.log(`Cambió de ${anterior} a ${nuevo}`)
})
// Función getter — para propiedades de reactive
const estado = reactive({ pagina: 1 })
watch(
() => estado.pagina,
(nuevaPagina) => {
cargarDatos(nuevaPagina)
}
)Opciones de watch()
watch(fuente, callback, {
immediate: true, // Ejecutar inmediatamente al montar (no lazy)
deep: true, // Observar cambios profundos en objetos/arrays
once: true, // Ejecutar solo una vez y luego detenerse
flush: 'post', // 'pre' (default), 'post' (después del DOM), 'sync'
})Observar múltiples fuentes
const x = ref(0)
const y = ref(0)
// Array de fuentes — callback recibe arrays de valores
watch([x, y], ([nuevoX, nuevoY], [anteriorX, anteriorY]) => {
console.log(`x: ${anteriorX}→${nuevoX}, y: ${anteriorY}→${nuevoY}`)
})Deep watching
Para observar cambios profundos dentro de un objeto:
const usuario = reactive({
nombre: 'Ana',
direccion: { ciudad: 'Madrid', codigo: '28001' }
})
// Sin deep: true, no detecta cambios en propiedades anidadas
watch(
() => usuario,
(val) => console.log('Usuario cambió', val),
{ deep: true }
)
// Mejor alternativa: observar la propiedad específica
watch(
() => usuario.direccion.ciudad,
(ciudad) => console.log('Ciudad:', ciudad)
)Limpieza con watch
Cuando un watcher necesita hacer operaciones asíncronas, puede haber condiciones de carrera. La función de limpieza se ejecuta antes de que el callback se vuelva a llamar:
watch(busqueda, (nueva, _anterior, onCleanup) => {
let cancelado = false
fetchResultados(nueva).then(datos => {
if (!cancelado) resultados.value = datos
})
onCleanup(() => {
cancelado = true // Cancela la solicitud anterior si busqueda cambia
})
})watchEffect() — rastreo automático de dependencias
watchEffect() ejecuta una función inmediatamente y rastrea automáticamente qué datos reactivos usa. Se vuelve a ejecutar cuando cualquiera de esos datos cambia:
import { watchEffect } from 'vue'
const usuario = ref<string | null>(null)
const pagina = ref(1)
// Se ejecuta inmediatamente y cuando usuario o pagina cambian
watchEffect(async () => {
if (!usuario.value) return
const datos = await fetch(`/api/users/${usuario.value}/posts?page=${pagina.value}`)
// Procesar datos...
})watchEffect con limpieza
watchEffect((onCleanup) => {
const subscription = storeDeEventos.suscribir(handler)
// Se llama antes de cada re-ejecución y al desmontar
onCleanup(() => subscription.cancelar())
})Detener un watcher
Tanto watch como watchEffect devuelven una función para detenerlos:
const detener = watchEffect(() => {
console.log(contador.value)
})
// Más tarde...
detener() // El watcher deja de rastrearLos watchers creados dentro de setup() o script setup se detienen automáticamente cuando el componente se desmonta. Los watchers creados fuera del ciclo de vida deben detenerse manualmente.
Patrones comunes con watchers
Debounce de búsqueda
import { ref, watch } from 'vue'
const busqueda = ref('')
const resultados = ref<string[]>([])
let timeout: ReturnType<typeof setTimeout>
watch(busqueda, (nueva) => {
clearTimeout(timeout)
timeout = setTimeout(async () => {
resultados.value = await buscarAPI(nueva)
}, 300)
})Persistir estado en localStorage
import { ref, watch } from 'vue'
function useLocalStorage<T>(clave: string, valorInicial: T) {
const guardado = localStorage.getItem(clave)
const estado = ref<T>(guardado ? JSON.parse(guardado) as T : valorInicial)
watch(estado, (nuevo) => {
localStorage.setItem(clave, JSON.stringify(nuevo))
}, { deep: true })
return estado
}
const preferencias = useLocalStorage('prefs', { tema: 'claro', idioma: 'es' })Sincronizar con una API externa
const idProducto = ref<number | null>(null)
const producto = ref<Producto | null>(null)
watch(idProducto, async (id) => {
if (id === null) {
producto.value = null
return
}
producto.value = await fetchProducto(id)
}, { immediate: true }) // Cargar inmediatamente si idProducto ya tiene valorPráctica
- Carrito de compras con computed: Crea un array reactivo de productos con precio y cantidad. Usa
computed()para calcular el subtotal, el IVA (21%) y el total. Añade un descuento reactivo y muestra todos los valores en una tabla. - Búsqueda con debounce: Implementa un input de búsqueda con
watch()que haga una petición ahttps://jsonplaceholder.typicode.com/users?name_like={busqueda}con 400ms de debounce. Muestra los resultados y un indicador de carga. - Sincronización con localStorage: Crea un formulario de preferencias (tema, idioma, tamaño de fuente) usando
reactive(). Usawatch()condeep: truepara guardar automáticamente en localStorage y restaurar al montar el componente.
En la siguiente lección aprenderemos a comunicar componentes mediante props y emits.
<script setup lang="ts">
import { ref, computed } from 'vue'
const precio = ref(100)
const cantidad = ref(3)
const descuento = ref(0.1) // 10%
// computed — valor derivado cacheado
const subtotal = computed(() => precio.value * cantidad.value)
const totalConDescuento = computed(() =>
subtotal.value * (1 - descuento.value)
)
const resumen = computed(() =>
`${cantidad.value} items × $${precio.value} = $${subtotal.value.toFixed(2)}`
)
// computed de escritura (writable computed)
const nombreCompleto = ref('Ana García')
const nombre = computed({
get: () => nombreCompleto.value.split(' ')[0],
set: (nuevoNombre: string) => {
const apellido = nombreCompleto.value.split(' ')[1] ?? ''
nombreCompleto.value = `${nuevoNombre} ${apellido}`.trim()
}
})
</script>
<template>
<div>
<p>{{ resumen }}</p>
<p>Total con {{ descuento * 100 }}% descuento: ${{ totalConDescuento.toFixed(2) }}</p>
<label>
Nombre (editable):
<input v-model="nombre" />
</label>
<p>Nombre completo: {{ nombreCompleto }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, watch, watchEffect, onUnmounted } from 'vue'
const busqueda = ref('')
const resultados = ref<string[]>([])
const cargando = ref(false)
const filtros = reactive({ categoria: 'todos', orden: 'asc' })
// watch — observa fuentes específicas
watch(busqueda, async (nuevo, anterior) => {
if (nuevo === anterior) return
cargando.value = true
// Simula una llamada API
await new Promise(r => setTimeout(r, 300))
resultados.value = nuevo
? [`Resultado para "${nuevo}" #1`, `Resultado para "${nuevo}" #2`]
: []
cargando.value = false
}, { debounce: 300 })
// Observar múltiples fuentes
watch(
[busqueda, () => filtros.categoria],
([nuevaBusqueda, nuevaCategoria]) => {
console.log('Cambio:', nuevaBusqueda, nuevaCategoria)
}
)
// watchEffect — rastrea dependencias automáticamente
const cleanup = watchEffect((onCleanup) => {
const timer = setInterval(() => {
console.log('tick con busqueda:', busqueda.value)
}, 1000)
// Limpieza cuando el efecto se vuelve a ejecutar o se desmonta
onCleanup(() => clearInterval(timer))
})
// Detener el watcher manualmente
onUnmounted(() => cleanup())
</script>
<template>
<div>
<input v-model="busqueda" placeholder="Buscar..." />
<p v-if="cargando">Buscando...</p>
<ul v-else>
<li v-for="r in resultados" :key="r">{{ r }}</li>
</ul>
</div>
</template>
Inicia sesión para guardar tu progreso