Reactividad
La reactividad es el motor de Vue: cuando cambia el estado, la UI se actualiza. En Composition API trabajas con ref, reactive, computed y watch en lugar de mutar el DOM a mano.
ref vs reactive
<script setup lang="ts">
import { ref, reactive } from 'vue'
const contador = ref(0)
const usuario = reactive({
nombre: 'Ana',
roles: ['editor'],
})
contador.value++
usuario.nombre = 'Luis'
usuario.roles.push('admin')
</script>
<template>
<p>{{ contador }}</p>
<p>{{ usuario.nombre }} — {{ usuario.roles.join(', ') }}</p>
</template>| API | Ideal para | Acceso en script |
|---|---|---|
ref(x) | primitivos, reasignaciones | .value |
reactive(obj) | objetos/estado agrupado | directo (sin .value) |
En el template, Vue desempaqueta ref automaticamente: {{ contador }} ya muestra el numero.
Cuando reasignar objetos
reactive no se reemplaza bien entero:
import { ref, reactive } from 'vue'
// Mal: pierdes la reactividad de la variable
let estado = reactive({ items: [] as string[] })
// estado = reactive({ items: ['a'] }) // rompe referencias
// Bien: muta o usa ref del objeto
const estadoRef = ref({ items: [] as string[] })
estadoRef.value = { items: ['a'] }Regla practica: ref por defecto; reactive cuando el objeto no se reasigna.
computed
Valores derivados en cache; solo se recalculan si cambian las dependencias.
<script setup lang="ts">
import { computed, ref } from 'vue'
const precio = ref(20)
const cantidad = ref(3)
const iva = 0.21
const total = computed(() => precio.value * cantidad.value * (1 + iva))
</script>
<template>
<p>Total: {{ total.toFixed(2) }} €</p>
</template>No metas efectos secundarios (fetch, logs) dentro de un computed; usa watch o funciones.
watch y watchEffect
import { ref, watch, watchEffect } from 'vue'
const query = ref('')
const resultados = ref<string[]>([])
watch(query, async (q, prev) => {
if (q === prev || q.length < 2) return
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`)
resultados.value = await res.json()
})
watchEffect(() => {
document.title = query.value ? `Buscar: ${query.value}` : 'Catalogo'
})watch: controlas la fuente y el callback.watchEffect: rastrea dependencias al ejecutarse; util para sincronizar.
Opciones frecuentes: { immediate: true, deep: true }.
Ciclo de vida
import { onMounted, onUnmounted, onUpdated } from 'vue'
let id: number | undefined
onMounted(() => {
id = window.setInterval(() => console.log('tick'), 5000)
})
onUnmounted(() => {
if (id) window.clearInterval(id)
})Equivalencias: onMounted ≈ mounted, onUnmounted ≈ unmounted.
shallowRef y triggerRef
Para estructuras grandes (canvas, instancias de librerias) donde no quieres deep tracking:
import { shallowRef, triggerRef } from 'vue'
const grafico = shallowRef({ puntos: [1, 2, 3] })
grafico.value.puntos.push(4) // Vue no detecta
triggerRef(grafico) // fuerza actualizacionBuenas practicas
- Deriva con
computed; no dupliques estado que se puede calcular. - Limpia timers y listeners en
onUnmounted. - Usa
watchcon filtros (debounce) en busquedas; evita spamear la API. - Prefiere inmutabilidad ligera (
estado.value = { ...estado.value, x: 1 }) si compartes refs. - Tipa
ref<T>()yreactive<T>()para atrapar errores en compile time.
Errores habituales
- Olvidar
.valueen el script (contador++no funciona sobre un ref). - Desestructurar
reactivesintoRefsy perder reactividad. watchprofundo en objetos enormes sin necesidad (coste de CPU).- Mutar un
computed(son de solo lectura salvo computed writable). - Crear estado compartido en un modulo sin
ref/reactivey esperar updates.
Ejercicios
- Implementa un contador con
refy uncomputedque muestre si es par/impar. - Haz un
watchsobre un input de busqueda que dispare solo si hay 3+ caracteres. - Sustituye un objeto
reactivereasignado por unrefy verifica que la UI sigue actualizando. - Registra un
setIntervalenonMountedy limpiao enonUnmounted.
Siguiente paso
En el capitulo 4 veras comunicacion padre-hijo con props/emits y como extraer logica a composables.
