Fundamentos de Reactividad
Preferencias de API
Esta página y muchos otros capítulos posteriores en la guía tienen contenido diferente para la Options API y la Composition API. Tu preferencia actual es Composition API. Puedes alternar entre los estilos de API usando el interruptor de "Preferencia de API" en la parte superior de la barra lateral izquierda.
Declarando el Estado Reactivo
ref()
En la Composition API, la manera recomendada de declarar el estado reactivo es utilizando la función ref()
:
js
import { ref } from 'vue'
const count = ref(0)
ref()
toma el argumento y lo retorna envuelto en un objeto ref con una propiedad .value
:
js
const count = ref(0)
console.log(count) // { value: 0 }
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
Mira también: Escribir Refs
Para acceder a las refs en la template de un componente, las declaramos y retornamos desde una función setup()
del componente:
js
import { ref } from 'vue'
export default {
// `setup` es un hook especial dedicado para la Composition API.
setup() {
const count = ref(0)
// expone la ref a la template
return {
count
}
}
}
template
<div>{{ count }}</div>
Observa que no necesitamos agregar .value
cuando usamos la ref en la template. Por conveniencia, las refs son automáticamente desempaquetadas cuando son usadas dentro de las templates (con unas pocas advertencias).
También puedes mutar una ref directamente en los manejadores de eventos:
template
<button @click="count++">
{{ count }}
</button>
Para lógica más compleja, podemos declarar funciones que muten las refs en el mismo ámbito y las expongan como métodos al lado del estado:
js
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
function increment() {
// .value es necesario en JavaScript
count.value++
}
// no olvides exponer también la función.
return {
count,
increment
}
}
}
Los métodos expuestos pueden ser usados entonces como manejadores de eventos:
template
<button @click="increment">
{{ count }}
</button>
Aquí está el ejemplo interactivo en Codepen, sin usar ninguna herramienta de construcción.
<script setup>
Exponer manualmente el estado y los métodos a través de setup()
puede ser engorroso. Por suerte, se puede evitar cuando se utilizan Componentes de un Solo Archivo (SFCs). Podemos simplificar el uso con <script setup>
:
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
<template>
<button @click="increment">
{{ count }}
</button>
</template>
Pruébalo en la Zona de Práctica
Las importaciones de nivel superior y las funciones declaradas en el <script setup>
son automáticamente usables en la template del mismo componente. Piensa en la template como una función de JavaScript declared en el mismo ámbito; naturalmente tiene acceso a todo lo que se declare junto a ella .
TIP
Por el resto de la guía, utilizaremos principalmente la sintaxis SFC + <script setup>
para los ejemplos de código de l a Composition API, ya que ese es el uso más común para los desarrolladores de Vue.
Si no utilizas SFC, puedes seguir utilizando Composition API con la opción setup()
.
¿Por qué Refs?
Puede que te estés preguntando por qué necesitamos refs con el .value
en lugar de variables planas. Para explicarlo, necesitaremos discutir brevemente cómo funciona el sistema de reactividad de Vue.
Cuando usas una ref en una template, y cambias el valor de la ref más tarde, Vue detecta automáticamente el cambio y actualiza el DOM en consecuencia. Esto es posible gracias a un sistema de reactividad basado en el seguimiento de dependencias. Cuando un componente se renderiza por primera vez, Vue rastrea cada referencia que se utilizó durante la renderización. Más tarde, cuando una referencia es mutada, esto disparará una nueva renderización para los componentes que la están rastreando.
En JavaScript estándar, no hay forma de detectar el acceso o mutación de variables planas. Sin embargo, podemos interceptar las operaciones get y set de las propiedades de un objeto utilizando métodos getter y setter.
La propiedad .value
le da a Vue la oportunidad de detectar cuando una ref ha sido accedida o mutada. Debajo del capó, Vue realiza el seguimiento en su getter, y realiza la activación en su setter. Conceptualmente, puedes pensar en una ref como un objeto que se parece a esto:
js
// pseudo código, no aplicación real
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}
Otra característica interesante de las refs es que, a diferencia de las variables planas, puedes pasar refs a funciones manteniendo el acceso al último valor y la conexión de reactividad. Esto es particularmente útil cuando se refactoriza lógica compleja en código reutilizable.
El sistema de reactividad se trata con más detalle en la sección Reactividad en Profundidad.
Reactividad Profunda
Las refs pueden contener cualquier tipo de valor, incluyendo objetos profundamente anidados, arrays o estructuras de datos integradas en JavaScript como Map
.
Una ref hará que su valor sea profundamente reactivo. Esto significa que puedes esperar que los cambios sean detectados incluso cuando mutes objetos o arrays anidados:
js
import { ref } from 'vue'
const obj = ref({
nested: { count: 0 },
arr: ['foo', 'bar']
})
function mutateDeeply() {
// esto trabajarña como se espera.
obj.value.nested.count++
obj.value.arr.push('baz')
}
Los valores no primitivos se convierten en proxies reactivos mediante reactive()
, que se explica más adelante.
También es posible excluir la reactividad profunda con shallow refs. En el caso de shallow refs, sólo se rastrea el acceso a .value
para la reactividad. Las shallow refs pueden utilizarse para optimizar el rendimiento evitando el coste de observación de objetos grandes, o en casos en los que el estado interno es gestionado por una biblioteca externa.
Lecturas adicionales:
Tiempo de Actualización del DOM
Cuando mutas el estado reactivo, el DOM se actualiza automáticamente. Sin embargo, se debería tener en cuenta que las actualizaciones del DOM no se aplican de forma sincrónica. En su lugar, Vue las almacena en búfer hasta la "siguiente marca" (next tick) del ciclo de actualización para garantizar que cada componente se actualice sólo una vez, independientemente de cuántos cambios de estado hayas realizado.
Para esperar a que se complete la actualización del DOM después de un cambio de estado, puedes usar la API global nextTick():
js
import { nextTick } from 'vue'
async function increment() {
count.value++
await nextTick()
// Ahora se actualiza el DOM
}
reactive()
Hay otra forma de declarar el estado reactivo, con la API reactive()
. A diferencia de una ref que envuelve el valor interno en un objeto especial, reactive()
hace que un objeto en sí mismo sea reactivo:
js
import { reactive } from 'vue'
const state = reactive({ count: 0 })
Consulta también: Escribir Reactive
Uso en la template:
template
<button @click="state.count++">
{{ state.count }}
</button>
Los objetos reactivos son Proxies de JavaScript y se comportan como objetos normales. La diferencia es que Vue es capaz de interceptar el acceso y la mutación de de un objeto reactivo para el seguimiento y la activación de la reactividad.
reactive()
convierte el objeto en profundidad: los objetos anidados también son empaquetados con reactive()
cuando se accede a ellos. Esto también es llamado internamente por ref()
cuando el valor de la referencia es un objeto. De forma similar a las shallow refs, también existe la API shallowReactive()
para optar por no utilizar la reactividad profunda.
Proxy Reactivo vs. Original
Es importante tener en cuenta que el valor devuelto por reactive()
es un Proxy del objeto original, que no es igual al objeto original:
js
const raw = {}
const proxy = reactive(raw)
// el proxy NO es igual al original.
console.log(proxy === raw) // false
Sólo el proxy es reactivo; mutar el objeto original no disparará actualizaciones. Por lo tanto, la mejor práctica cuando se trabaja con el sistema de reactividad de Vue es utilizar exclusivamente las versiones proxy de tu estado.
Para asegurar un acceso consistente al proxy, llamar a reactive()
sobre el mismo objeto siempre devuelve el mismo proxy, y llamar a reactive()
sobre un proxy existente también devuelve ese mismo proxy:
js
// llamar a reactive() sobre el mismo objeto devuelve el mismo proxy
console.log(reactive(raw) === proxy) // true
// llamar a reactive() sobre un proxy se devuelve a sí mismo
console.log(reactive(proxy) === proxy) // true
Esta regla se aplica también a los objetos anidados. Debido a la reactividad profunda, los objetos anidados dentro de un objeto reactivo también son proxies:
js
const proxy = reactive({})
const raw = {}
proxy.nested = raw
console.log(proxy.nested === raw) // false
Limitaciones de reactive()
La API reactive()
tiene algunas limitaciones:
Tipos de valor limitados: sólo funciona para tipos de objeto (objetos, arrays y tipos de colección como
Map
ySet
). No puede contener tipos primitivos comostring
,number
oboolean
.No se puede reemplazar el objeto entero: dado que el seguimiento de la reactividad de Vue funciona sobre el acceso a propiedades, debemos mantener siempre la misma referencia al objeto reactivo. Esto significa que no podemos «reemplazar» fácilmente un objeto reactivo porque se pierde la conexión de reactividad con la primera referencia:
jslet state = reactive({ count: 0 }) // la referencia anterior ({ count: 0 }) ya no es seguida // (¡se ha perdido la conexión de reactividad!) state = reactive({ count: 1 })
No desestructurable: cuando desestructuremos una propiedad de tipo primitivo de un objeto reactivo en variables locales, o cuando pasemos esa propiedad a una función, perderemos la conexión de reactividad:
jsconst state = reactive({ count: 0 }) // count se desconecta de state.count cuando se desestructura. let { count } = state // no afecta el estado original count++ // la función recibe un número plano y // no podrá seguir los cambios en state.count // tenemos que pasar el objeto completo para mantener la reactividad callSomeFunction(state.count)
Debido a estas limitaciones, recomendamos utilizar ref()
como API principal para declarar el estado reactivo.
Detalles Adicionales de Desempaquetado de Ref
Como Propiedad de Objeto Reactivo
Una ref se desempaqueta automáticamente cuando se accede a ella o se muta como propiedad de un objeto reactivo. En otras palabras, se comporta como una propiedad normal:
js
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
Si se asigna una nueva ref a una propiedad vinculada a una ref existente, ésta reemplazará a la ref anterior:
js
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// la ref original está ahora desconectada de state.count
console.log(count.value) // 1
El desempaquetado de ref. sólo ocurre cuando está anidado dentro de un objeto reactivo profundo. No se aplica cuando es accesado como una propiedad de un objeto shallow reactive.
Advertencia en Arrays y Colecciones
A diferencia de los objetos reactivos, no se realiza ningún desempaquetado cuando la ref es accedida como un elemento de un array reactivo o un tipo de colección nativa como Map
:
js
const books = reactive([ref('Guía de Vue 3')])
// necesita .value aquí
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// necesita .value aquí
console.log(map.get('count').value)
Advertencia al Desempaquetar en las Templates
El desempaquetado de ref en templates sólo se aplica si la ref es una propiedad de nivel superior en el contexto de renderizado del template.
En el siguiente ejemplo, count
y object
son propiedades de nivel superior, pero object.id
no lo es:
js
const count = ref(0)
const object = { id: ref(1) }
Por lo tanto, esta expresión funciona como se esperaba:
template
{{ count + 1 }}
...mientras que ésta NO:
template
{{ object.id + 1 }}
El resultado renderizado será [objeto Objeto]1
porque objeto.id
no se desempaqueta al evaluar la expresión y sigue siendo un objeto ref. Para solucionar esto, podemos desestructurar id
en una propiedad de nivel superior:
js
const { id } = object
template
{{ id + 1 }}
Ahora el resultado renderizado será 2
.
Otra cosa a tener en cuenta es que una referencia se desempaqueta si es el valor final evaluado de una interpolación de texto (es decir, una etiqueta {{ }}
), por lo que lo siguiente mostrará 1
:
template
{{ object.id }}
Esto es sólo una característica de conveniencia de la interpolación de texto y es equivalente a {{ object.id.value }}
.