En esta página

ref() y reactive() — el sistema de reactividad

14 min lectura TextoCap. 2 — 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 template

Por 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.value

ref() 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' // Detectado

reactive() — 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 reactivos

Limitaciones 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í funciona

toRef() 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 cambia

toRefs() 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 10

Esta 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 DOM

Cuá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 reactividad

Reactividad 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

  1. Experimenta con ref y reactive: Crea un componente con un ref de tipo objeto y un reactive. Muestra ambos en el template. Intenta desestructurar el reactive sin toRefs y observa el problema.
  2. 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.
  3. shallowRef en práctica: Crea un shallowRef con 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.

Regla de oro — ref() para todo
Si no estás seguro de cuándo usar ref() vs reactive(), usa siempre ref(). Es más predecible, funciona con primitivos y objetos, y se puede extraer desde composables sin perder reactividad. El equipo de Vue menciona que incluso ellos prefieren ref() en la mayoría de casos.
No desestructures reactive() directamente
Si desestructuras un objeto reactive(), pierdes la reactividad: const { nombre } = estado (donde estado es reactive) hace que nombre sea un string plano, no reactivo. Usa toRefs(estado) o toRef(estado, 'nombre') para mantener la reactividad al desestructurar.
El Proxy de ES6 — cómo funciona Vue internamente
Vue 3 usa Proxy de ES6 para implementar la reactividad. Cuando accedes a una propiedad de un objeto reactive, el Proxy registra esa dependencia. Cuando la propiedad cambia, el Proxy notifica a todos los efectos (computed, watch, render) que dependen de ella. Esto es más eficiente que el sistema de Object.defineProperty de Vue 2.
vue
<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>
typescript
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  // ✅