Vue 3 te ofrece dos formas de crear estado reactivo: ref y reactive. Ambas hacen los datos reactivos para que la plantilla se actualice cuando cambian, pero funcionan de forma distinta y tienen restricciones diferentes.
ref: funciona con cualquier valor
ref envuelve cualquier valor — un número, un string, un booleano, un objeto, un array o null. Se accede y modifica a través de .value en JavaScript/TypeScript. En las plantillas, Vue lo desenvuelve automáticamente para que no escribas .value allí.
import { ref } from 'vue'
const count = ref(0) // primitivo
const user = ref({ name: '' }) // objeto
const items = ref<string[]>([]) // array
count.value++ // acceso a través de .value en JS
user.value.name = 'Ana' // acceso anidado
items.value.push('new item')<template>
<!-- No se necesita .value en las plantillas -->
<p>{{ count }}</p>
<p>{{ user.name }}</p>
</template>reactive: solo objetos, sin .value
reactive envuelve un objeto (o array, Map, Set) y hace sus propiedades reactivas directamente — sin .value. Pero solo funciona con objetos. No puedes pasarle un número o string.
import { reactive } from 'vue'
const state = reactive({ count: 0, name: '' })
state.count++ // no se necesita .value
state.name = 'Ana'La gran limitación: no puedes reasignar un objeto reactive. Si haces state = newObject, rompes el enlace reactivo. Los watchers y la plantilla siguen referenciando el objeto antiguo.
let state = reactive({ count: 0 })
state = reactive({ count: 1 }) // ❌ rompe la reactividad — los watchers antiguos siguen observando el proxy anteriorCuándo usar cuál
Usar ref para todo es el valor predeterminado más seguro. Funciona con cualquier tipo, puedes reasignarlo libremente (count.value = newValue), y maneja todos los casos límite. El .value es un pequeño coste por la flexibilidad.
Usar reactive cuando tienes un grupo de propiedades relacionadas que siempre van juntas y nunca reemplazarás el objeto completo — como un formulario:
const form = reactive({
email: '',
password: '',
remember: false
})
form.email = 'ana@example.com' // ergonómico, sin .valueLos errores comunes
Desestructurar un reactive rompe la reactividad:
const state = reactive({ count: 0 })
const { count } = state // ❌ count es ahora un número plano (0), no reactivo
// Usa toRefs() si necesitas desestructurarReasignar un ref está bien, reasignar un reactive no:
const data = ref<User[]>([])
data.value = await fetchUsers() // ✅ funciona perfectamente
let data = reactive<User[]>([])
data = await fetchUsers() // ❌ rompe el enlace reactivoPor eso ref es el valor predeterminado recomendado. Cuando necesitas reemplazar el valor completo (respuestas de API, resetear estado), ref simplemente funciona.
Ver también: ¿Cuál es la diferencia entre computed y watch? · ¿Por qué pierdo reactividad al desestructurar?
Referencias
- ref() - Vue.js docs
- reactive() - Vue.js docs
- Reactivity Fundamentals - Vue.js docs