Saltar al contenido
schedule 12 min Vue

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.

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

Contador.vue
<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? Porque ref() 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.

@include('front.web-course.components.code-block', [ 'language' => 'javascript', 'filename' => 'MisionForm.vue', 'code' => ' ' ])

Otro ejemplo práctico — un objeto de configuración:

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

ConRef.vue
<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>
ConReactive.vue
<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. Usa reactive() 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 solo ref() 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:

MisionControl.vue
<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 .value en 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.
code

Panel de Control de Misión Espacial

Medio schedule 20 min

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-model para 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.

Newsletter

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