En esta página
ref() y reactive() — el sistema de reactividad
El sistema de reactividad de Vue 3
Vue 3 construyó su sistema de reactividad desde cero usando Proxies de ES6. Cuando creas un dato reactivo, Vue lo envuelve en un Proxy que intercepta lecturas y escrituras. Así puede rastrear qué partes del template o qué efectos dependen de cada dato, y actualizarlos eficientemente cuando el dato cambia.
Hay dos APIs principales para crear datos reactivos: ref() y reactive().
ref() — la API universal
ref() toma un valor y devuelve un objeto reactivo con una propiedad .value:
import { ref } from 'vue'
const contador = ref(0) // Ref<number>
const mensaje = ref('Hola') // Ref<string>
const activo = ref(false) // Ref<boolean>
const lista = ref<number[]>([]) // Ref<number[]>
// Para leer o escribir, siempre usa .value en JS/TS
console.log(contador.value) // 0
contador.value = 5
contador.value++
// En el TEMPLATE, Vue desenvuelve .value automáticamente
// <p>{{ contador }}</p> ← sin .value en el templatePor qué existe .value
El envoltura en un objeto es necesaria para que los primitivos (números, strings, booleans) sean reactivos. Los primitivos en JavaScript se pasan por valor, no por referencia. Al envolverlos en un objeto, Vue puede interceptar el acceso mediante el Proxy.
// Sin ref, esto no sería reactivo
let contador = 0
// Vue no puede saber cuándo cambia este valor
// Con ref, el Proxy intercepta .value
const contador = ref(0)
// Vue detecta cada vez que lees o escribes contador.valueref() con objetos
ref() también funciona con objetos. Vue convierte automáticamente el objeto interno en reactive():
const usuario = ref({
nombre: 'Ana',
edad: 28,
direccion: { ciudad: 'Madrid' }
})
// El objeto interno también es reactivo (deep)
usuario.value.nombre = 'Carlos' // Detectado
usuario.value.direccion.ciudad = 'Sevilla' // Detectadoreactive() — para objetos con múltiples propiedades
reactive() convierte directamente un objeto en un Proxy reactivo. No necesitas .value:
import { reactive } from 'vue'
const estado = reactive({
cargando: false,
datos: [] as string[],
error: null as string | null,
pagina: 1,
})
// Sin .value — acceso directo
estado.cargando = true
estado.datos.push('Item')
estado.pagina++reactive() es más ergonómico cuando manejas un grupo de propiedades relacionadas. Es especialmente popular para el estado de formularios.
La trampa de desestructurar reactive()
El problema más común con reactive() es la pérdida de reactividad al desestructurar:
const estado = reactive({ contador: 0, nombre: 'Vue' })
// ❌ PROBLEMA: counter y nombre son primitivos, pierden reactividad
const { contador, nombre } = estado
// ✅ SOLUCIÓN: toRefs convierte cada propiedad en un ref
const { contador, nombre } = toRefs(estado)
// Ahora contador.value y nombre.value son reactivosLimitaciones de reactive()
reactive() solo funciona con objetos (incluidos arrays y Maps). No puede hacer reactivos primitivos:
// ❌ Esto no funciona
const contador = reactive(0) // Error en runtime
// ✅ Para primitivos, usa ref()
const contador = ref(0)Tampoco puedes reasignar el objeto reactivo entero, solo sus propiedades:
let estado = reactive({ valor: 0 })
// ❌ Esto rompe la reactividad — el Proxy queda huérfano
estado = reactive({ valor: 1 })
// ✅ Modifica propiedades individuales
estado.valor = 1
// ✅ O usa ref() si necesitas reasignar el objeto completo
const estado = ref({ valor: 0 })
estado.value = { valor: 1 } // Esto sí funcionatoRef() y toRefs()
toRef() crea un ref reactivo a partir de una propiedad de un objeto reactive:
const estado = reactive({ nombre: 'Vue', version: 3 })
// Un solo ref enlazado a la propiedad
const nombre = toRef(estado, 'nombre')
nombre.value = 'Nuxt' // estado.nombre también cambia
estado.nombre = 'React' // nombre.value también cambiatoRefs() convierte todas las propiedades de un reactive en refs:
const estado = reactive({ x: 0, y: 0, z: 0 })
// Desestructuración segura — todos siguen siendo reactivos
const { x, y, z } = toRefs(estado)
x.value = 10 // estado.x también es 10Esta es la forma estándar de exponer propiedades reactivas desde un composable sin perder reactividad.
shallowRef() — reactividad superficial
shallowRef() solo rastrea si .value cambia (reasignación), no los cambios internos del objeto:
import { shallowRef } from 'vue'
// Solo la reasignación de .value dispara actualizaciones
const config = shallowRef({ tema: 'oscuro', idioma: 'es' })
// ❌ NO dispara actualización — Vue no rastrea propiedades internas
config.value.tema = 'claro'
// ✅ SÍ dispara actualización — se reasigna .value completo
config.value = { tema: 'claro', idioma: 'es' }
// También puedes forzar actualización con triggerRef
import { triggerRef } from 'vue'
config.value.tema = 'claro'
triggerRef(config) // Fuerza la actualizaciónÚsalo cuando tienes objetos muy grandes y sabes que solo harás reasignaciones completas, no mutaciones de propiedades.
shallowReactive() — reactive superficial
Similar a shallowRef pero para reactive. Solo rastrea las propiedades de primer nivel:
import { shallowReactive } from 'vue'
const estado = shallowReactive({
nivel1: 'reactivo',
anidado: { nivel2: 'NO reactivo' }
})
estado.nivel1 = 'cambio detectado' // ✅ Actualiza
estado.anidado.nivel2 = 'sin cambio' // ❌ No actualiza el DOMCuándo usar ref() vs reactive()
| Situación | Recomendación |
|---|---|
| Valor primitivo (string, number, boolean) | ref() siempre |
| Objeto con propiedades relacionadas (formulario, estado de UI) | reactive() |
| Objeto que puedes necesitar reasignar completamente | ref() |
| Exponer datos desde un composable | ref() (más predecible al desestructurar) |
| No sabes qué usar | ref() — siempre funciona |
Funciones de utilidad de reactividad
Vue exporta varias funciones para inspeccionar y manipular refs:
import { isRef, isReactive, unref, toRaw } from 'vue'
const x = ref(5)
const obj = reactive({ a: 1 })
isRef(x) // true
isRef(obj) // false
isReactive(obj) // true
// unref — desenvuelve un ref o devuelve el valor si no es ref
unref(x) // 5 (equivale a isRef(x) ? x.value : x)
unref(5) // 5
// toRaw — obtiene el objeto original sin el Proxy (para depuración)
toRaw(obj) // { a: 1 } sin reactividadReactividad con Map y Set
reactive() también funciona con colecciones de ES6:
const mapa = reactive(new Map<string, number>())
const conjunto = reactive(new Set<string>())
// Estas operaciones son reactivas
mapa.set('a', 1)
conjunto.add('vue')
mapa.delete('a')
conjunto.clear()Práctica
- Experimenta con ref y reactive: Crea un componente con un
refde tipo objeto y unreactive. Muestra ambos en el template. Intenta desestructurar elreactivesintoRefsy observa el problema. - Formulario reactivo: Crea un formulario de registro con
reactive({ nombre, email, password, confirmPassword }). Valida que las contraseñas coincidan usando una función que lea el estado reactivo. - shallowRef en práctica: Crea un
shallowRefcon un objeto de configuración complejo. Intenta mutar una propiedad interna y confirma que el DOM no actualiza. Luego implementa la actualización correcta con reasignación completa.
En la siguiente lección veremos computed() para estado derivado y los watchers para efectos secundarios.
<script setup lang="ts">
import { ref, reactive, toRef, toRefs, shallowRef } from 'vue'
// ref() — para valores primitivos y objetos simples
const contador = ref(0)
const nombre = ref('Vue')
const lista = ref<string[]>([])
// En JS siempre necesitas .value
function incrementar(): void {
contador.value++
lista.value.push(`Item ${contador.value}`)
}
// reactive() — para objetos con múltiples propiedades relacionadas
const formulario = reactive({
email: '',
password: '',
recordar: false,
})
// toRefs — extraer refs de un reactive sin perder reactividad
const { email, password } = toRefs(formulario)
// shallowRef — reactividad superficial (solo el valor raíz)
const config = shallowRef({ tema: 'oscuro', idioma: 'es' })
function cambiarTema(): void {
// Esto SÍ dispara actualizaciones (reasignación completa)
config.value = { ...config.value, tema: 'claro' }
}
</script>
<template>
<div>
<p>Contador: {{ contador }}</p>
<p>Email: {{ email }}</p>
<button type="button" @click="incrementar">+</button>
<button type="button" @click="cambiarTema">Cambiar tema</button>
<form>
<input v-model="formulario.email" placeholder="Email" />
<input v-model="formulario.password" type="password" />
</form>
</div>
</template>
import { reactive, isReactive, isRef, toRaw } from 'vue'
// Vue envuelve los objetos en un Proxy de ES6
const estado = reactive({ cuenta: 100 })
console.log(isReactive(estado)) // true
console.log(isReactive({})) // false
// toRaw — obtener el objeto original sin el Proxy
const raw = toRaw(estado)
console.log(isReactive(raw)) // false
// Precaución: mutar el raw NO dispara actualizaciones
raw.cuenta = 200 // ❌ Vue no detecta esto
// Usar el proxy sí dispara actualizaciones
estado.cuenta = 200 // ✅
Inicia sesión para guardar tu progreso