Reactividad: ref y reactive
La reactividad es el superpoder de Vue. Ya has usado ref() para hacer datos reactivos, pero hay más en la historia. Entender cuándo usar ref() vs reactive() — y cómo Vue rastrea los cambios internamente — te ahorrará horas de debugging.
Cómo funciona la reactividad de Vue
Vue utiliza JavaScript Proxies para interceptar cuándo lees o escribes propiedades en tus datos reactivos. Cuando un valor reactivo cambia, Vue sabe exactamente qué partes del template necesitan re-renderizarse. No necesitas actualizar el DOM manualmente — Vue se encarga de todo.
Este sistema es lo que hace que Vue sea tan productivo: tú cambias los datos, y la interfaz se actualiza sola. Sin document.querySelector(), sin innerHTML, sin manipulación manual del DOM.
ref() en profundidad
ref() funciona con cualquier tipo de valor: strings, números, booleanos, arrays, objetos, e incluso null. Envuelve tu valor en un objeto reactivo con una propiedad .value.
<script setup>
import { ref } from 'vue'
// Números
const contador = ref(0)
const temperatura = ref(36.6)
// Strings
const nombre = ref('Astronauta')
// Booleanos
const motorEncendido = ref(false)
// Arrays
const tripulacion = ref(['Ana', 'Carlos', 'Luna'])
// Objetos
const nave = ref({ nombre: 'Stellar-7', velocidad: 0 })
// Null (valor pendiente, como una respuesta de API)
const datosOrbit = ref(null)
</script>
En el <script setup>, siempre accedes al valor con .value. En el template, Vue hace el unwrap automático — no necesitas escribir .value:
<script setup>
import { ref } from 'vue'
const contador = ref(0)
const motorEncendido = ref(false)
const tripulacion = ref(['Ana', 'Carlos', 'Luna'])
// En script: siempre .value
const incrementar = () => {
contador.value++
}
const toggleMotor = () => {
motorEncendido.value = !motorEncendido.value
}
const agregarTripulante = (nombre) => {
tripulacion.value.push(nombre)
}
</script>
<template>
<!-- En template: sin .value -->
<p>Contador: {{ contador }}</p>
<p>Motor: {{ motorEncendido ? 'Encendido' : 'Apagado' }}</p>
<p>Tripulantes: {{ tripulacion.length }}</p>
<button @click="incrementar">+1</button>
<button @click="toggleMotor">Toggle motor</button>
<button @click="agregarTripulante('Nuevo')">Agregar tripulante</button>
</template>
¿Por qué se necesita
.value? Porqueref()envuelve tu valor en un objeto{ value: tuValor }. Ese objeto wrapper es lo que Vue puede rastrear. Si usaras una variable primitiva directamente (let x = 5), Vue no tendría forma de detectar cuándo la cambias.
reactive() en profundidad
reactive() funciona solo con objetos (incluyendo arrays). La diferencia principal: no necesitas .value — accedes a las propiedades directamente.
Tripulantes: {{ formulario.tripulantes }}
Estado: {{ formulario.aprobada ? \'Aprobada\' : \'Pendiente\' }}
' ])Otro ejemplo práctico — un objeto de configuración:
<script setup>
import { reactive } from 'vue'
const ajustes = reactive({
tema: 'oscuro',
idioma: 'es',
notificaciones: true,
volumen: 80
})
const resetearAjustes = () => {
// Puedes modificar propiedades individualmente
ajustes.tema = 'claro'
ajustes.idioma = 'es'
ajustes.notificaciones = true
ajustes.volumen = 50
}
</script>
Limitaciones de reactive()
Hay dos trampas importantes que debes conocer:
1. No puedes reasignar el objeto completo. Si lo haces, pierdes la reactividad:
// MAL: reasignar el objeto rompe la reactividad
let estado = reactive({ nombre: 'Apolo', fase: 1 })
// Esto NO funciona como esperas
estado = reactive({ nombre: 'Artemis', fase: 2 })
// La variable "estado" ahora apunta a un objeto nuevo,
// pero el template sigue conectado al viejo
2. Destructurar pierde la reactividad:
const mision = reactive({ nombre: 'Apolo', fase: 1 })
// MAL: al destructurar, obtienes valores planos (no reactivos)
const { nombre, fase } = mision
// "nombre" es ahora un string normal, no reactivo
// Si mision.nombre cambia, "nombre" NO se actualiza
ref() vs reactive() — cuándo usar cada uno
| Característica | ref() |
reactive() |
|---|---|---|
| Tipos soportados | Cualquiera (string, number, object, array...) | Solo objetos y arrays |
| Acceso en script | .value |
Directo |
| Acceso en template | Automático (sin .value) |
Directo |
| Reasignar el valor completo | Funciona | Pierde reactividad |
| Destructurar | No aplica (es un solo valor) | Pierde reactividad |
Veamos el mismo ejemplo implementado con ambos enfoques:
<script setup>
import { ref } from 'vue'
// Con ref() — cada dato es independiente
const nombreMision = ref('Artemis III')
const destino = ref('Luna')
const fechaLanzamiento = ref('2027-09-01')
const tripulantes = ref(4)
// Reasignar funciona sin problemas
const cargarDatosAPI = (datos) => {
nombreMision.value = datos.nombre
destino.value = datos.destino
fechaLanzamiento.value = datos.fecha
tripulantes.value = datos.tripulantes
}
</script>
<script setup>
import { reactive } from 'vue'
// Con reactive() — todo agrupado en un objeto
const mision = reactive({
nombre: 'Artemis III',
destino: 'Luna',
fechaLanzamiento: '2027-09-01',
tripulantes: 4
})
// Para actualizar, modificas las propiedades (no reasignas el objeto)
const cargarDatosAPI = (datos) => {
Object.assign(mision, datos)
}
</script>
Recomendación: usa
ref()por defecto — funciona con todo y es más predecible. Usareactive()cuando tengas un grupo de datos relacionados (como un formulario o configuración) y no necesites reasignar el objeto completo. Muchos equipos profesionales prefieren usar soloref()para mantener la consistencia.
toRefs() y toRef(): recuperar la reactividad
¿Recuerdas que destructurar un reactive() pierde la reactividad? toRefs() resuelve ese problema. Convierte cada propiedad de un objeto reactivo en un ref() individual que mantiene la conexión:
<script setup>
import { reactive, toRefs, toRef } from 'vue'
const mision = reactive({
nombre: 'Odyssey',
destino: 'Europa (luna de Júpiter)',
estado: 'preparación',
tripulantes: 6
})
// toRefs() — convierte TODAS las propiedades en refs
const { nombre, destino, estado, tripulantes } = toRefs(mision)
// Ahora cada variable es un ref reactivo
// Cambiar nombre.value TAMBIÉN cambia mision.nombre
nombre.value = 'Odyssey II'
console.log(mision.nombre) // 'Odyssey II' — están conectados
// toRef() — convierte UNA sola propiedad
const soloEstado = toRef(mision, 'estado')
soloEstado.value = 'en órbita'
console.log(mision.estado) // 'en órbita'
</script>
<template>
<!-- Puedes usar las refs destructuradas directamente -->
<h2>{{ nombre }}</h2>
<p>Destino: {{ destino }}</p>
<p>Estado: {{ estado }}</p>
<p>Tripulantes: {{ tripulantes }}</p>
</template>
toRefs() es especialmente útil cuando quieres pasar propiedades de un objeto reactivo a composables o funciones que esperan refs individuales.
shallowRef() y shallowReactive()
Cuando trabajas con objetos grandes (como una lista de 10.000 elementos de una API), la reactividad profunda puede afectar el rendimiento. Para esos casos existen las versiones "shallow" (superficiales):
import { shallowRef, shallowReactive } from 'vue'
// shallowRef: solo detecta cambios cuando reasignas .value
const listaEnorme = shallowRef([/* miles de elementos */])
// Esto SÍ dispara una actualización
listaEnorme.value = [...listaEnorme.value, nuevoElemento]
// Esto NO dispara actualización (mutación interna)
listaEnorme.value.push(nuevoElemento) // No reactivo
// shallowReactive: solo el primer nivel es reactivo
const config = shallowReactive({
tema: 'oscuro', // reactivo
avanzado: { // este objeto interno NO es reactivo
depurar: false
}
})
No te preocupes demasiado por esto ahora. Solo recuerda que existen para cuando necesites optimizar el rendimiento con datos muy grandes.
Errores comunes (y cómo evitarlos)
1. Olvidar .value en el script
import { ref } from 'vue'
const puntos = ref(0)
// MAL
const sumar = () => {
puntos++ // No funciona — puntos es un objeto ref, no un número
}
// BIEN
const sumar = () => {
puntos.value++
}
2. Destructurar reactive() y perder la reactividad
import { reactive, toRefs } from 'vue'
const nave = reactive({ nombre: 'Falcon', velocidad: 0 })
// MAL — pierdes reactividad
const { nombre, velocidad } = nave
// BIEN — usa toRefs()
const { nombre, velocidad } = toRefs(nave)
3. Reasignar un objeto reactive() completo
import { reactive, ref } from 'vue'
let estado = reactive({ fase: 1, combustible: 100 })
// MAL — reasignar rompe la conexión con el template
estado = reactive({ fase: 2, combustible: 80 })
// BIEN — modifica las propiedades
estado.fase = 2
estado.combustible = 80
// BIEN — usa Object.assign
Object.assign(estado, { fase: 2, combustible: 80 })
// ALTERNATIVA — usa ref() si necesitas reasignar
const estado2 = ref({ fase: 1, combustible: 100 })
estado2.value = { fase: 2, combustible: 80 } // Esto sí funciona
Resumen
ref()— funciona con cualquier tipo, necesita.valueen script, unwrap automático en template. Es tu opción por defecto.reactive()— solo objetos/arrays, acceso directo sin.value, pero no puedes reasignar ni destructurar sin perder reactividad.toRefs()/toRef()— convierten propiedades de un objeto reactivo en refs individuales manteniendo la conexión.shallowRef()/shallowReactive()— versiones con reactividad solo superficial, para optimizar rendimiento con datos grandes.- Regla de oro: ante la duda, usa
ref(). Es más versátil y predecible.
Panel de Control de Misión Espacial
Construye un dashboard de Mission Control que combine ref(), reactive() y toRefs(). El componente debe tener:
- Con
ref()— stats individuales de la misión:cuentaRegresiva(number): empieza en 10, un botón la reduce en 1 (mínimo 0).estado(string): alterna entre"en espera","lanzamiento"y"en órbita"con un botón.tripulacion(number): cantidad de tripulantes, con botones +/- (mínimo 1, máximo 8).
- Con
reactive()— detalles de la misión agrupados en un objeto:nombre,destino,fechaLanzamiento.- Usa inputs con
v-modelpara editar cada campo.
- Con
toRefs()— extrae las propiedades del objeto reactivo en refs individuales y muéstralas en una sección separada del template para demostrar que siguen conectadas. - Un botón "Resetear misión" que ponga todos los valores a su estado inicial.
lightbulb Pistas
Para alternar el estado entre tres valores, puedes usar un array ['en espera', 'lanzamiento', 'en órbita'] y un índice que incrementes con módulo: indice = (indice + 1) % 3. Para toRefs(), recuerda que const { nombre, destino } = toRefs(misionDetalles) crea refs que puedes usar directamente en el template y que siguen conectados al objeto original.