Actualizar objetos y arrays
En resumen: Un objeto o un array en el estado de Vue se cambia en el sitio, porque Vue vigila los cambios mediante un proxy; un array nuevo en .value también funciona, pero un array normal sin ref no.
Problema
«Quitar» no quita nada
El carrito tiene tres líneas, cada una con un botón «Quitar». Pulsa «Quitar» en el pan.
Consola
La consola dice líneas: 2. La pantalla muestra las tres líneas y el total viejo. Los datos cambiaron, la pantalla no los sigue, y no hay ningún error.
remove pone en cart un array nuevo sin el pan. El valor nuevo está en la variable. ¿Por qué Vue no dibuja?La idea
Cómo ve la biblioteca un cambio
Con un número está claro: 1 y 2 son distintos. Un array y un objeto, en cambio, son referencias: la variable apunta a un objeto en la memoria. Hay dos preguntas. ¿La variable apunta a otro objeto? ¿Y cambió el contenido del mismo objeto? Cada biblioteca pregunta a su manera, y aquí la diferencia se nota en el código.
Vue hace la segunda pregunta. ref([...]) envuelve el array en un proxy: un objeto que se pone delante del array y ve cada lectura y cada escritura. push, splice y line.qty++ pasan por el proxy. Él registra el cambio y avisa a la plantilla que lee el array para que se ejecute otra vez. Por eso en Vue el array se cambia en el sitio, y es la forma normal.
Pero el proxy no ve la variable que apunta a él. En el ejemplo let cart = reactive([...]) y después cart = cart.filter(...): la variable apunta ahora a un array nuevo y normal. La plantilla lee el array viejo mediante el proxy viejo, y nadie le avisa. Con const cart = ref([...]) el array nuevo va a cart.value, y la ref ve ese cambio.
Míralo
Mira: el mismo array o uno nuevo
<script setup>const cart = ref([bread]);
function addMilk() { cart.value.push(milk);}
function addCheese() { cart.value = [...cart.value, cheese];}</script>Paso a paso, en palabras
- cart es una ref. Dentro está el array, envuelto en un proxy: un objeto que se pone delante del array y ve cada lectura y cada escritura.
- cart.value.push(milk) pasa por el proxy. El proxy registra: el array cambió.
- El proxy avisa a la plantilla, que lee cart, de que tiene que ejecutarse otra vez.
- La plantilla se ejecuta y la pantalla muestra las dos líneas.
- Así también vale: un array nuevo en cart.value. La ref nota el cambio de .value.
- La ref avisa a la plantilla, y la plantilla se ejecuta otra vez con el array nuevo.
- La pantalla muestra tres líneas. En Vue funcionan los dos caminos.
- La regla en Vue: cambia a través de la ref. Vue no ve una variable normal ni la copia de un número.
push por el proxy del array y el array nuevo por .value. Vue se pierde solo lo que pasa por fuera de los dos.La idea
Un cambio a través del proxy
El carrito tiene tres acciones: añadir una línea, cambiar la cantidad de una línea, quitar una línea.
// añadircart.value.push({ ...product, qty: 1 });
// cambiar una línea: la línea también es un proxyconst line = cart.value.find((l) => l.id === id);line.qty++;
// quitarconst index = cart.value.findIndex((l) => l.id === id);cart.value.splice(index, 1);- Vue vigila también los objetos dentro del array:
linedecart.valuees un proxy yline.qty++se ve. - El otro camino también funciona: un array nuevo en
cart.value, por ejemplocart.value = cart.value.filter((l) => l.id !== id). La ref ve el cambio de.value. - Un cambio que pasa por fuera de la ref no se ve: una variable normal con un array nuevo, o una copia de un número.
Pruébalo
Arregla «Quitar»
Este es el carrito del principio. Arregla remove para que la línea desaparezca y el total se calcule de nuevo. La comprobación quita el pan y después el queso.
Consola
Solución
App.vue<script setup>
import { ref } from 'vue';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const cart = ref([
{ id: 1, name: 'Pan blanco', price: 1.2, qty: 2 },
{ id: 5, name: 'Leche fresca', price: 1.6, qty: 1 },
{ id: 7, name: 'Queso blanco', price: 7.9, qty: 1 },
]);
function remove(id) {
cart.value = cart.value.filter((line) => line.id !== id);
}
const total = () => cart.value.reduce((sum, line) => sum + line.price * line.qty, 0);
</script>
<template>
<main>
<h2>Tu carrito</h2>
<ul>
<li v-for="line in cart" :key="line.id">
{{ line.name }}: {{ line.qty }} × {{ money(line.price) }}
<button type="button" @click="remove(line.id)" :aria-label="`Quitar ${line.name}`">
Quitar
</button>
</li>
</ul>
<p>Total: {{ money(total()) }}</p>
</main>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
h2 { font-size: 18px; margin: 16px 0 4px; }
ul { padding-left: 20px; }
li { margin: 6px 0; }
button { font: inherit; padding: 2px 10px; margin-left: 6px; }
let cart = reactive(...) pasa a ser const cart = ref(...) con import { ref } from 'vue', y remove es cart.value = cart.value.filter((line) => line.id !== id). splice mediante cart.value también funciona. El total lee cart.value, así que también se ejecuta de nuevo. Un array normal sin ref no funciona: splice lo cambia, pero Vue no lo vigila.Pruébalo
Encuentra el error
«+» no sube la cantidad. ¿Qué línea tiene la culpa?
Elige la línea con el error.
Línea 2: let { qty } = line copia el número en una variable nueva. qty++ cambia la copia, y la línea del carrito queda igual. Debería ser line.qty++.
La idea
Una línea del array
La clienta pulsa «+» en el pan. Tiene que cambiar el qty de un objeto, y el objeto está en un array.
En Vue line.qty++ es la forma correcta: la línea de cart.value es un proxy y Vue ve el cambio. El error es sacar el número del objeto:
let { qty } = line; // una copia del númeroqty++; // cambia la copiaLa desestructuración y let n = line.qty dan una copia. Vue vigila el objeto line, no cada variable por la que ha pasado el número. Escribe en el objeto: line.qty++.
Más precisamente, para curiosos
Vigilancia profunda. ref y reactive hacen que también se vigilen los objetos de dentro, la primera vez que se leen. shallowRef vigila solo .value: con ella no se ve un cambio interno, y pones un array nuevo, como en React.
reactive y la desestructuración. const { total } = reactive({ total: 0 }) da un número, no un valor vigilado. toRefs(obj) convierte los campos en refs que siguen conectadas al objeto.
El array nuevo. cart.value = [...cart.value, line] funciona y hace un array nuevo, como harías en React. Es útil cuando el array viejo tiene que quedar igual, por ejemplo para «Deshacer».
structuredClone no acepta un proxy: structuredClone(cart.value) lanza un error. Primero saca el array normal con toRaw(cart.value).
Pruébalo
«+» y «−»
Escribe more y less. «+» sube la cantidad de la línea en 1, y «−» la baja en 1. Cuando la cantidad es 1, «−» quita la línea. El total sigue a la cantidad.
Consola
Solución
App.vue<script setup>
import { ref } from 'vue';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
const cart = ref([
{ id: 1, name: 'Pan blanco', price: 1.2, qty: 2 },
{ id: 5, name: 'Leche fresca', price: 1.6, qty: 1 },
{ id: 7, name: 'Queso blanco', price: 7.9, qty: 1 },
]);
function more(id) {
const line = cart.value.find((l) => l.id === id);
line.qty++;
}
function less(id) {
const line = cart.value.find((l) => l.id === id);
if (line.qty === 1) {
remove(id);
} else {
line.qty--;
}
}
function remove(id) {
const index = cart.value.findIndex((line) => line.id === id);
cart.value.splice(index, 1);
}
const total = () => cart.value.reduce((sum, line) => sum + line.price * line.qty, 0);
</script>
<template>
<main>
<h2>Tu carrito</h2>
<ul>
<li v-for="line in cart" :key="line.id">
{{ line.name }}: {{ line.qty }} × {{ money(line.price) }}
<button type="button" @click="less(line.id)" :aria-label="`Menos ${line.name}`">−</button>
<button type="button" @click="more(line.id)" :aria-label="`Más ${line.name}`">+</button>
<button type="button" @click="remove(line.id)" :aria-label="`Quitar ${line.name}`">
Quitar
</button>
</li>
</ul>
<p>Total: {{ money(total()) }}</p>
</main>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
h2 { font-size: 18px; margin: 16px 0 4px; }
ul { padding-left: 20px; }
li { margin: 6px 0; }
button { font: inherit; padding: 2px 10px; margin-left: 6px; }
more encuentra la línea en cart.value y hace line.qty++. less llama a remove cuando qty === 1; si no, hace line.qty--. Con let { qty } = line y qty++ la línea queda igual: cambia una copia. Sin la comprobación del 1, la línea llega a «0 ×» y se queda en el carrito.
Ejercicio
Catálogo y carrito
El catálogo tiene cuatro productos con un botón «Añadir» (el nombre para el lector de pantalla: «Añadir Pan blanco»). Escribe add y remove. El primer clic en «Añadir» hace una línea con cantidad 1, y los siguientes suben la cantidad de la misma línea en lugar de hacer una nueva. «Quitar» quita la línea. Cuando el carrito está vacío, dice «El carrito está vacío».
Consola
Solución
App.vue<script setup>
import { ref } from 'vue';
const money = (x) => x.toFixed(2).replace('.', ',') + ' €';
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 },
{ id: 10, name: 'Manzanas', price: 2.1 },
];
const cart = ref([]);
function add(product) {
const found = cart.value.find((line) => line.id === product.id);
if (found) {
found.qty++;
} else {
cart.value.push({ ...product, qty: 1 });
}
}
function remove(id) {
const index = cart.value.findIndex((line) => line.id === id);
cart.value.splice(index, 1);
}
const total = () => cart.value.reduce((sum, line) => sum + line.price * line.qty, 0);
</script>
<template>
<main>
<h1>La tienda</h1>
<ul>
<li v-for="p in products" :key="p.id">
{{ p.name }}, {{ money(p.price) }}
<button type="button" @click="add(p)" :aria-label="`Añadir ${p.name}`">
Añadir
</button>
</li>
</ul>
<h2>Tu carrito</h2>
<p v-if="cart.length === 0">El carrito está vacío</p>
<ul aria-label="Tu carrito">
<li v-for="line in cart" :key="line.id">
{{ line.name }}: {{ line.qty }} × {{ money(line.price) }}
<button type="button" @click="remove(line.id)" :aria-label="`Quitar ${line.name}`">
Quitar
</button>
</li>
</ul>
<p>Total: {{ money(total()) }}</p>
</main>
</template>
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 8px 16px; }
h1 { font-size: 22px; }
h2 { font-size: 18px; margin: 16px 0 4px; }
ul { padding-left: 20px; }
li { margin: 6px 0; }
button { font: inherit; padding: 2px 10px; margin-left: 6px; }
add busca la línea con find. Si está: found.qty++. Si no: cart.value.push({ ...product, qty: 1 }). { ...product, qty: 1 } es un objeto nuevo, y el producto del catálogo no se toca. remove es splice o filter mediante cart.value. Con const cart = [] sin ref el carrito se queda vacío en la pantalla, y con solo push cada clic añade una línea nueva para el mismo producto.Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: El primer componente · Listas y claves · Condiciones: vacío, error, cargando · Estado
- Continúa en: Estado derivado · Estado compartido: hacia arriba · Estado compartido · El escaparate de la tienda
- Ejercicios sobre el tema: Estado