Computed y watchers
Ya sabes almacenar datos reactivos con ref() y reactive(). Pero, ¿qué pasa cuando necesitas un valor que depende de otros valores? ¿O cuando quieres ejecutar algo cada vez que un dato cambia? Para eso existen computed y los watchers — dos herramientas fundamentales que usarás en prácticamente todos tus componentes.
computed() — estado derivado reactivo
Un computed es un valor que se calcula automáticamente a partir de otros valores reactivos. Cuando sus dependencias cambian, el computed se recalcula solo. Piénsalo como una fórmula de Excel: cambias una celda y todas las celdas que dependen de ella se actualizan.
<script setup>
import { ref, computed } from 'vue'
const nombre = ref('Valentina')
const apellido = ref('Tereshkova')
// computed: se recalcula automáticamente cuando nombre o apellido cambian
const nombreCompleto = computed(() => {
return `${nombre.value} ${apellido.value}`
})
// nombreCompleto.value => "Valentina Tereshkova"
// Si cambias nombre.value = 'Yuri', nombreCompleto se actualiza solo
</script>
<template>
<input v-model="nombre" placeholder="Nombre">
<input v-model="apellido" placeholder="Apellido">
<p>Astronauta: {{ nombreCompleto }}</p>
</template>
Veamos un ejemplo más práctico — una lista de productos filtrada y un carrito con precio total en nuestra cafetería espacial:
<script setup>
import { ref, computed } from 'vue'
const busqueda = ref('')
const menu = ref([
{ nombre: 'Café Nebulosa', precio: 3.50, categoria: 'bebida' },
{ nombre: 'Latte Lunar', precio: 4.20, categoria: 'bebida' },
{ nombre: 'Croissant Cósmico', precio: 2.80, categoria: 'comida' },
{ nombre: 'Tostada Estelar', precio: 3.00, categoria: 'comida' },
{ nombre: 'Smoothie Saturno', precio: 5.50, categoria: 'bebida' }
])
const carrito = ref([
{ nombre: 'Café Nebulosa', precio: 3.50, cantidad: 2 },
{ nombre: 'Croissant Cósmico', precio: 2.80, cantidad: 1 }
])
// Filtrar productos por búsqueda
const menuFiltrado = computed(() => {
if (!busqueda.value) return menu.value
const termino = busqueda.value.toLowerCase()
return menu.value.filter(item =>
item.nombre.toLowerCase().includes(termino)
)
})
// Total de artículos en el carrito
const totalArticulos = computed(() => {
return carrito.value.reduce((sum, item) => sum + item.cantidad, 0)
})
// Precio total del carrito
const precioTotal = computed(() => {
return carrito.value.reduce((sum, item) => {
return sum + (item.precio * item.cantidad)
}, 0).toFixed(2)
})
</script>
<template>
<input v-model="busqueda" placeholder="Buscar en el menú...">
<ul>
<li v-for="item in menuFiltrado" :key="item.nombre">
{{ item.nombre }} — {{ item.precio }}€
</li>
</ul>
<p>Artículos en carrito: {{ totalArticulos }}</p>
<p>Total: {{ precioTotal }}€</p>
</template>
computed es cacheado (y eso importa)
Una diferencia crucial entre computed y una función normal: el computed se cachea. Solo se recalcula cuando alguna de sus dependencias reactivas cambia. Una función se ejecuta cada vez que el template se re-renderiza:
<script setup>
import { ref, computed } from 'vue'
const productos = ref([/* lista grande de productos */])
// BIEN: computed — se cachea, solo recalcula si "productos" cambia
const totalComputed = computed(() => {
console.log('Calculando total (computed)...')
return productos.value.reduce((sum, p) => sum + p.precio, 0)
})
// FUNCIONA PERO ES MENOS EFICIENTE: función — se ejecuta en cada render
const totalFuncion = () => {
console.log('Calculando total (función)...')
return productos.value.reduce((sum, p) => sum + p.precio, 0)
}
</script>
<template>
<!-- totalComputed solo se recalcula cuando productos cambia -->
<p>Total (computed): {{ totalComputed }}</p>
<!-- totalFuncion() se ejecuta CADA VEZ que el template se renderiza -->
<p>Total (función): {{ totalFuncion() }}</p>
</template>
Regla: si necesitas derivar un valor a partir de datos reactivos, usa siempre
computed(). Si necesitas ejecutar lógica (como un evento click), usa una función. Los computed son de solo lectura por defecto — existen computed con escritura, pero rara vez los necesitarás.
watch() — reaccionar a cambios
Mientras que computed es para derivar datos, watch es para ejecutar efectos secundarios cuando un dato cambia. Cosas como: hacer una llamada a una API, guardar en localStorage, disparar una animación o registrar un log.
<script setup>
import { ref, watch } from 'vue'
const busqueda = ref('')
const resultados = ref([])
// watch básico: observa "busqueda" y ejecuta el callback cuando cambia
watch(busqueda, (nuevoValor, valorAnterior) => {
console.log(`Búsqueda cambió de "${valorAnterior}" a "${nuevoValor}"`)
})
</script>
La sintaxis es: watch(fuente, (nuevoValor, valorAnterior) => { ... }). Veamos un ejemplo más completo — guardar la búsqueda en localStorage y hacer una llamada a la API con debounce:
<script setup>
import { ref, watch } from 'vue'
const busqueda = ref('')
const resultados = ref([])
const cargando = ref(false)
let timeoutId = null
watch(busqueda, (nuevoValor) => {
// Guardar en localStorage cada vez que cambie
localStorage.setItem('ultimaBusqueda', nuevoValor)
// Debounce: esperar 300ms antes de llamar a la API
clearTimeout(timeoutId)
if (nuevoValor.length < 2) {
resultados.value = []
return
}
timeoutId = setTimeout(async () => {
cargando.value = true
try {
const resp = await fetch(`/api/menu?q=${nuevoValor}`)
resultados.value = await resp.json()
} finally {
cargando.value = false
}
}, 300)
})
</script>
<template>
<input v-model="busqueda" placeholder="Buscar en Café Estelar...">
<p v-if="cargando">Buscando...</p>
<ul v-else>
<li v-for="item in resultados" :key="item.id">
{{ item.nombre }}
</li>
</ul>
</template>
Observar propiedades de un objeto reactivo
Si quieres observar una propiedad específica de un objeto reactive(), necesitas usar una función getter como fuente:
import { reactive, watch } from 'vue'
const pedido = reactive({
bebida: 'Café Nebulosa',
tamaño: 'mediano',
extras: []
})
// Observar una propiedad específica — usa una función getter
watch(() => pedido.bebida, (nueva, anterior) => {
console.log(`Bebida cambiada: ${anterior} → ${nueva}`)
})
// Observar múltiples fuentes a la vez
watch(
[() => pedido.bebida, () => pedido.tamaño],
([nuevaBebida, nuevoTamaño], [antBebida, antTamaño]) => {
console.log('Pedido actualizado')
}
)
Opciones de watch
watch() acepta un tercer argumento con opciones útiles:
import { ref, watch } from 'vue'
const filtros = ref({ categoria: 'bebida', ordenar: 'precio' })
// immediate: ejecuta el callback inmediatamente al montar el componente
watch(filtros, (nuevoValor) => {
console.log('Filtros actualizados:', nuevoValor)
}, { immediate: true })
// deep: observa cambios profundos en objetos anidados
const config = ref({
display: { modo: 'oscuro', fuente: 14 },
sonido: { volumen: 80, notificaciones: true }
})
watch(config, (nuevoValor) => {
localStorage.setItem('config', JSON.stringify(nuevoValor))
}, { deep: true })
Nota:
{ deep: true }recorre todo el objeto en cada verificación, así que úsalo con precaución en objetos muy grandes. Si solo te interesa una propiedad específica, es mejor usar un getter:watch(() => config.value.display.modo, callback).
watchEffect() — rastreo automático de dependencias
watchEffect() es el hermano más relajado de watch(). Se ejecuta inmediatamente y luego se re-ejecuta cada vez que cualquier dependencia reactiva usada dentro de él cambie. No necesitas decirle qué observar — Vue lo detecta automáticamente:
<script setup>
import { ref, watchEffect } from 'vue'
const nombreCafe = ref('Café Estelar')
const pedidosPendientes = ref(3)
// watchEffect detecta automáticamente que usa nombreCafe y pedidosPendientes
// Se ejecuta ahora Y cada vez que cualquiera de los dos cambie
watchEffect(() => {
document.title = `${nombreCafe.value} (${pedidosPendientes.value} pedidos)`
})
// Al montar: document.title = "Café Estelar (3 pedidos)"
// Si pedidosPendientes.value = 5 → "Café Estelar (5 pedidos)"
</script>
Otro ejemplo práctico — sincronizar datos con una API:
import { ref, watchEffect } from 'vue'
const categoriaActiva = ref('bebidas')
const pagina = ref(1)
const productos = ref([])
// Se re-ejecuta cuando categoriaActiva O pagina cambian
watchEffect(async () => {
const resp = await fetch(
`/api/productos?cat=${categoriaActiva.value}&page=${pagina.value}`
)
productos.value = await resp.json()
})
watch vs watchEffect: cuándo usar cada uno
| Característica | watch() |
watchEffect() |
|---|---|---|
| Especificar dependencias | Manual (tú las defines) | Automático (Vue las detecta) |
| Acceso a valor anterior | Sí (oldValue) |
No |
| Ejecución inicial | No (salvo con immediate: true) |
Sí, siempre |
| Mejor para | Comparar valores, lógica condicional | Sincronizar efectos con múltiples dependencias |
computed vs watch — la regla de oro
Esta es la distinción más importante de esta lección:
computed()— para derivar datos. "X depende de Y". Ejemplo: el precio total depende de los artículos del carrito.watch()/watchEffect()— para ejecutar efectos secundarios. "Cuando Y cambie, hacer algo". Ejemplo: cuando el carrito cambie, guardarlo en localStorage.
<script setup>
import { ref, computed, watch } from 'vue'
const carrito = ref([
{ nombre: 'Latte Lunar', precio: 4.20, cantidad: 2 },
{ nombre: 'Croissant Cósmico', precio: 2.80, cantidad: 1 }
])
// COMPUTED: derivar datos del carrito
const precioTotal = computed(() => {
return carrito.value.reduce((sum, i) => sum + i.precio * i.cantidad, 0)
})
const envioGratis = computed(() => precioTotal.value > 30)
const resumenPedido = computed(() => {
return `${carrito.value.length} productos — ${precioTotal.value.toFixed(2)}€`
})
// WATCH: ejecutar efectos secundarios cuando el carrito cambie
watch(carrito, (nuevoCarrito) => {
// Guardar en localStorage
localStorage.setItem('carrito', JSON.stringify(nuevoCarrito))
// Enviar analytics
console.log('Carrito actualizado:', nuevoCarrito.length, 'items')
}, { deep: true })
</script>
<template>
<p>{{ resumenPedido }}</p>
<p v-if="envioGratis">Envío gratis incluido</p>
</template>
Recuerda: si puedes expresarlo como "X es una transformación de Y", usa
computed. Si necesitas "hacer algo cuando Y cambie", usawatch. Nunca useswatchpara actualizar unref— eso es trabajo decomputed.
Resumen
computed()— crea valores derivados que se recalculan automáticamente y se cachean. Perfecto para filtros, totales, formateos y cualquier dato que dependa de otros.watch()— ejecuta un callback cuando una fuente específica cambia. Te da acceso al valor anterior y al nuevo. Ideal para llamadas a APIs, localStorage y otros efectos secundarios.watchEffect()— se ejecuta inmediatamente y rastrea sus dependencias automáticamente. Perfecto para sincronizar efectos que dependen de múltiples fuentes reactivas.- Regla de oro:
computedpara derivar datos,watch/watchEffectpara ejecutar efectos secundarios.
Carrito de Café Estelar
Construye el carrito de compras de Café Estelar combinando computed, watch y watchEffect. El componente debe tener:
- Un array reactivo
carritocon objetos{ nombre, precio, cantidad }(al menos 3 productos iniciales del menú estelar). - Botones para aumentar/disminuir la cantidad de cada producto (mínimo 0 — si llega a 0, se elimina del carrito).
- Con
computed():totalArticulos: suma total de todas las cantidades.precioTotal: suma de precio × cantidad de cada producto, formateado a 2 decimales.envioGratis:truesi el total supera los 30€,falseen caso contrario.
- Con
watch(): cada vez que el carrito cambie, guárdalo enlocalStoragecon la clave'carritoEstelar'. - Con
watchEffect(): actualizadocument.titlepara que muestre"Café Estelar (X artículos)"donde X estotalArticulos.
lightbulb Pistas
Para eliminar un producto cuando su cantidad llega a 0, puedes usar carrito.value = carrito.value.filter(item => item.cantidad > 0) dentro de la función que reduce la cantidad. Recuerda usar { deep: true } en el watch del carrito para detectar cambios en las cantidades. Para watchEffect, simplemente accede a totalArticulos.value dentro del callback y Vue rastreará la dependencia automáticamente.