Estado
En resumen: El estado es un valor que un componente recuerda entre renders; en Vue se guarda con ref, y cada cambio actualiza el componente en la página.
Problema
El botón no cuenta
La tarjeta del producto tiene un botón «Añadir» y un contador «En el carrito». Pulsa «Añadir» tres veces.
Consola
La consola dice count = 1, 2, 3. La tarjeta dice 0. No hay ningún error. La variable count cambia, pero la pantalla no la sigue. ¿Dónde está la diferencia?
La idea
Una variable normal no basta
La tarjeta no la dibuja tu código, sino la biblioteca. Solo vuelve a dibujar cuando sabe que algo cambió. count++ cambia la variable, pero no avisa a nadie.
<script setup> se ejecuta una vez por cada tarjeta, y la plantilla la ejecuta Vue. Vue vuelve a ejecutar la plantilla solo cuando cambia algo que vigila. Vue no vigila una variable let normal. El clic cambia count, pero la plantilla no se vuelve a ejecutar, así que la tarjeta se queda en 0.
Necesitas datos que el componente recuerde entre dos renders y cuyo cambio lo vuelva a dibujar. Esto se llama estado.
La idea
useState y ref
<script setup>import { ref } from 'vue';
const count = ref(0);
function add() { count.value++;}</script>
<template> <p>En el carrito: {{ count }}</p></template>ref(0)devuelve una caja, y el número está encount.value. En el script escribes.value.- En la plantilla Vue desenvuelve la caja: escribes
{{ count }}, sin.value. - Cuando la plantilla lee
count, Vue lo recuerda. Cuandocount.valuecambia, Vue vuelve a ejecutar la plantilla.
El manejador del clic se da como función: @click="add". En Vue también funcionan @click="add()" o @click="count++": la plantilla los envuelve en una función que se ejecuta al hacer clic.
Consola
Míralo
Mira qué pasa con un clic
<script setup>const count = ref(0);
function addThree() { count.value++; count.value++; count.value++;}</script>
<template> <article> <button @click="addThree">+3</button> <p>En el carrito: {{ count }}</p> </article></template>Paso a paso, en palabras
- La tarjeta guarda count en una ref: una caja cuyo valor es count.value. Vue vigila quién la lee.
- Vue ejecuta la plantilla. Lee count y devuelve una descripción: un botón y un texto con 0. Vue recuerda que la plantilla lee count.
- La primera vez, la descripción se convierte en elementos reales en el DOM. La página ya muestra «En el carrito: 0».
- Un clic en +3: el navegador llama a addThree.
- Tres veces count.value++. La caja cambia al instante: 1, 2, 3. Vue planea ejecutar la plantilla otra vez, pero solo una.
- El manejador terminó. Vue ejecuta la plantilla una vez, con count = 3, y devuelve una descripción nueva.
- Vue compara la descripción nueva con la vieja. El botón es el mismo, solo cambió el texto. En el DOM cambia solo el texto.
- El contador muestra 3. El cambio de una ref es inmediato, y el dibujo ocurre una vez, después del manejador.
La tarjeta no se construye de nuevo. La biblioteca compara la descripción nueva con la vieja y cambia en el DOM solo la diferencia: el texto. El botón es el mismo elemento y el foco se queda en él. Esa es la diferencia con render() del módulo del DOM, que lo reemplazaba todo.
Pruébalo
Arregla el contador
Esta es la tarjeta del principio. Haz que count sea estado, para que después de cada clic en «Añadir» la tarjeta muestre el número nuevo.
Consola
Solución
ProductCard.vue<script setup>
import { ref } from 'vue';
defineProps({ product: Object });
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const count = ref(0);
function add() {
count.value++;
}
</script>
<template>
<article class="card">
<h2>{{ product.name }}</h2>
<p>{{ money(product.price) }}</p>
<button type="button" @click="add" :aria-label="`Añadir ${product.name}`">
Añadir
</button>
<p>En el carrito: {{ count }}</p>
</article>
</template>
App.vue<script setup>
import ProductCard from './ProductCard.vue';
const product = { id: 1, name: 'Pan blanco', price: 1.2 };
</script>
<template>
<main>
<h1>La tienda</h1>
<ProductCard :product="product" />
</main>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
let count = 0 pasa a ser const count = ref(0), con import { ref } from 'vue', y en add escribes count.value++. La plantilla conserva {{ count }}: con {{ count.value }} el texto sale vacío, porque ahí count ya es el número.La idea
La caja y el número
El agua mineral también se vende en un pack de 6 botellas. Sería cómodo que el botón del pack llamara seis veces a add(). Aquí las dos bibliotecas se comportan de forma distinta.
count.value es siempre el número actual, así que seis veces count.value++ da 6. La plantilla no se ejecuta seis veces por eso. Vue junta los cambios y la ejecuta una vez, después del manejador.
Aquí el error es otro. La caja es count, y count.value es un número. Un número puesto en otra variable es una copia:
let bottles = count.value;bottles += 6; // cambia la copia, count queda igualVue vigila la caja, no cada variable por la que ha pasado el número. Escribe en la caja: count.value += 6.
Pruébalo
Encuentra el error
«Añadir un pack» debería añadir 6 botellas. ¿Qué línea tiene la culpa?
Elige la línea con el error.
Línea 5: cambia bottles, una copia del número. La caja count no se toca y la plantilla no se ejecuta. Debería ser count.value += 6.
Pruébalo
Un pack de agua
Escribe addBox() para que «Añadir un pack (6 uds.)» sume 6 botellas a lo que ya hay en el carrito. La comprobación pulsa «Añadir» una vez, luego el pack dos veces, y espera 1, 7 y 13.
Consola
Solución
ProductCard.vue<script setup>
import { ref } from 'vue';
defineProps({ product: Object });
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const count = ref(0);
function add() {
count.value++;
}
function addBox() {
count.value += 6;
}
</script>
<template>
<article class="card">
<h2>{{ product.name }}</h2>
<p>{{ money(product.price) }}</p>
<button type="button" @click="add" :aria-label="`Añadir ${product.name}`">
Añadir
</button>
<button type="button" @click="addBox">Añadir un pack (6 uds.)</button>
<p>En el carrito: {{ count }}</p>
</article>
</template>
App.vue<script setup>
import ProductCard from './ProductCard.vue';
const product = { id: 15, name: 'Agua mineral', price: 0.9 };
</script>
<template>
<main>
<h1>La tienda</h1>
<ProductCard :product="product" />
</main>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
count.value += 6 cambia la caja y la plantilla se vuelve a ejecutar. let n = count.value; n += 6 cambia una copia y la tarjeta no se mueve. count.value = 6 no suma, pone 6, y después de «Añadir» el contador es 6, no 7.La idea
Cada tarjeta recuerda lo suyo
El estado pertenece a la instancia, no al código. El catálogo tiene dos tarjetas del mismo ProductCard, y cada una tiene su propio count:
Consola
Pulsa «Añadir» dos veces en el pan y una en la leche: 2 y 1. La biblioteca guarda el estado de cada tarjeta según su lugar en el árbol de componentes. Si quitas la tarjeta de la página, su estado se va con ella. Cuando dos tarjetas tienen que mostrar el mismo número, este tiene que vivir en su padre común. De eso trata la lección sobre estado compartido de este módulo.
Más precisamente, para curiosos
ref y reactive. reactive({ count: 0 }) hace que el objeto se vigile y no tiene .value, pero solo funciona con objetos y no se puede reemplazar el objeto entero. ref funciona con cualquier valor, por eso la documentación de Vue la recomienda como la principal.
Cuándo se ejecuta la plantilla. Vue vuelve a ejecutar la plantilla en la siguiente microtarea, cuando termina el código que se está ejecutando ahora. await nextTick() espera a ese render, por ejemplo para leer el DOM nuevo.
Desenvolver. En la plantilla se desenvuelven las refs del nivel superior de <script setup>. Una ref dentro de un objeto normal ({ total: ref(0) }) no se desenvuelve en una expresión como {{ cart.total + 1 }}.
Options API. El código antiguo de Vue escribe el componente como un objeto con data(), methods y this.count. El código nuevo usa <script setup> y la Composition API.
Ejercicio
Elige la cantidad
El catálogo tiene tres tarjetas. En cada una añade una cantidad que empieza en 1, un botón «−» (nombre para el lector de pantalla: «Menos Pan blanco»), un botón «+» («Más Pan blanco») y una línea «2 × 1,20 € = 2,40 €» con la cantidad, el precio y el total. «−» está desactivado cuando la cantidad es 1. Cada tarjeta cuenta por su cuenta.
Consola
Solución
ProductCard.vue<script setup>
import { ref } from 'vue';
defineProps({ product: Object });
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const count = ref(1);
function less() {
count.value--;
}
function more() {
count.value++;
}
</script>
<template>
<article class="card">
<h2>{{ product.name }}</h2>
<p>{{ money(product.price) }}</p>
<p>
<button type="button" @click="less" :disabled="count === 1" :aria-label="`Menos ${product.name}`">−</button>
<button type="button" @click="more" :aria-label="`Más ${product.name}`">+</button>
</p>
<p>{{ count }} × {{ money(product.price) }} = {{ money(count * product.price) }}</p>
</article>
</template>
App.vue<script setup>
import ProductCard from './ProductCard.vue';
const products = [
{ id: 1, name: 'Pan blanco', price: 1.2 },
{ id: 5, name: 'Leche fresca', price: 1.6 },
{ id: 7, name: 'Queso blanco', price: 7.9 },
];
</script>
<template>
<main>
<h1>La tienda</h1>
<ProductCard v-for="p in products" :key="p.id" :product="p" />
</main>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
.card { border: 1px solid #ddd5c4; border-radius: 8px; padding: 4px 16px 12px; margin-bottom: 12px; max-width: 340px; }
.card h2 { font-size: 18px; margin: 8px 0 4px; }
.card p { margin: 6px 0; }
button { font: inherit; padding: 4px 12px; margin-right: 6px; }
ProductCard.vue: const count = ref(1), less hace count.value--, y more hace count.value++. «−» es :disabled="count === 1": así la cantidad no puede bajar de 1. El total es money(count * product.price) directamente en la plantilla: se deduce de count y no es un estado aparte, de lo que trata la lección sobre estado derivado. Si la ref está en un <script> normal aparte, es una sola para todas las tarjetas, y «+» en el pan cambia también el queso.Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Eventos · El primer componente · Props: datos desde arriba · Listas y claves
- Continúa en: Actualizar objetos y arrays · Estado derivado · Estado compartido: hacia arriba · Efectos
- Ejercicios sobre el tema: Estado