Saltar al contenido
schedule 10 min Vue

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.

NombreCompleto.vue
<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:

CafeEstelar.vue
<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:

CacheDemo.vue
<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.

BuscadorEstelar.vue
<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:

BuscadorConAPI.vue
<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:

TituloReactivo.vue
<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.
ComputedVsWatch.vue
<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", usa watch. Nunca uses watch para actualizar un ref — eso es trabajo de computed.

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: computed para derivar datos, watch/watchEffect para ejecutar efectos secundarios.
code

Carrito de Café Estelar

Medio schedule 20 min

Construye el carrito de compras de Café Estelar combinando computed, watch y watchEffect. El componente debe tener:

  • Un array reactivo carrito con 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: true si el total supera los 30€, false en caso contrario.
  • Con watch(): cada vez que el carrito cambie, guárdalo en localStorage con la clave 'carritoEstelar'.
  • Con watchEffect(): actualiza document.title para que muestre "Café Estelar (X artículos)" donde X es totalArticulos.
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.

Newsletter

Recibe nuevos cursos, actualizaciones, artículos del blog y promociones en tu correo.