Ir al contenido
wannacode

Listas y claves

En resumen: Una lista en Vue se hace con v-for a partir de un array de componentes, y :key le dice a Vue qué fila vieja es cada fila nueva; un número aleatorio es una mala clave, y el índice también lo es en cuanto la lista cambia.

Problema

Quité el pan y la leche pasó a ser 3

El comprador tiene pan, leche y queso en el carrito. Escribe 3 para el pan y 2 para la leche, y después quita el pan. Pruébalo: cambia los dos números y pulsa «Quitar» junto al pan.

Vue

El pan ya no está, pero la leche ahora es 3 y el queso es 2. En la consola no hay ningún error. Los datos son correctos: se quitó justo el pan. Solo los campos están mezclados. La causa es un solo atributo en la línea 22 de App.vue: lo que el código le dice a la biblioteca sobre cada fila de la lista.

La idea

Una lista en un componente

En el módulo «JavaScript en la página» una lista se hacía a partir de un array con map y createElement. En un componente la idea es la misma, pero cada elemento del array pasa a ser parte del marcado:

<ul>
<li v-for="p in items" :key="p.id">
<ProductCard :product="p" />
</li>
</ul>

v-for="p in items" repite <li> para cada producto, y dentro p es el producto actual. :key es la clave con la que Vue reconoce cada fila.

Cuando los datos cambian, el componente devuelve una lista nueva. La biblioteca no borra la vieja para dibujarla otra vez. Compara la vieja con la nueva y cambia en la página solo la diferencia. Por eso el foco y lo escrito en los campos se conservan, en lugar de perderse como con render() en el módulo «JavaScript en la página».

Para comparar tiene que saber qué fila vieja corresponde a qué fila nueva. Eso es lo que dice la clave.

La idea

Emparejar por clave

La biblioteca empareja las filas viejas y nuevas por la clave:

  • La misma clave es la misma fila. El elemento de la página se queda, y la biblioteca cambia dentro solo lo que difiere en los datos: el nombre, el precio.
  • Una clave que no está en la lista nueva es una fila quitada. Su elemento se quita de la página.
  • Una clave nueva es una fila nueva. Se crea un elemento nuevo.

El elemento que se queda conserva todo lo suyo: lo escrito en un <input>, el foco, un menú desplegable abierto. Eso no está en los datos sino en el propio elemento. Por eso importa que se quede con su producto.

En el ejemplo la clave es el índice: el lugar en el array. Tras «Quitar», la leche está en el lugar 0. La fila vieja con la clave 0 era la del pan. La biblioteca la conserva, cambia su nombre a «Leche fresca» y deja el campo con el 3.

Míralo

Ver el emparejamiento

A la izquierda está la página antes de «Quitar», a la derecha la lista nueva del componente. Fíjate en qué fila desaparece.

<!-- key = el lugar en la lista -->
<li v-for="(p, index) in items" :key="index">
<ProductCard :product="p" />
</li>
<!-- key = el id del producto -->
<li v-for="p in items" :key="p.id">
<ProductCard :product="p" />
</li>
Paso a paso, en palabras
  1. Tres filas en el carrito, y la clave es el lugar: 0, 1, 2. El comprador ha escrito 3, 2 y 1 en los campos. Lo escrito vive en el elemento <input>, no en los datos.
  2. «Quitar Pan blanco». Los datos ya tienen dos productos, y el componente devuelve una lista nueva: la clave 0 es la leche, la clave 1 es el queso.
  3. La biblioteca empareja la lista vieja y la nueva por la clave: 0 con 0, 1 con 1. La clave 2 no está en la nueva, así que desaparece la tercera fila: la del queso.
  4. Las filas que se quedan reciben los datos nuevos, y sus encabezados ahora dicen leche y queso. Los campos son los viejos, con 3 y 2. La cantidad del pan está ahora en la leche.
  5. El mismo carrito con key = id. Las claves son 1, 2, 3 y son del producto, no del lugar.
  6. Otra vez «Quitar Pan blanco». La lista nueva tiene las claves 2 y 3.
  7. La clave 2 se empareja con la 2, y la 3 con la 3, aunque hayan subido. Falta la clave 1, así que desaparece la fila del pan, junto con su campo.
  8. Cada campo se ha quedado con su producto: leche 2, queso 1. La clave le dice a la biblioteca quién es quién, no dónde está.

Con el índice desaparece la última fila, y las dos primeras cambian de nombre. Con el id desaparece la fila del pan, y las otras dos solo suben con sus campos. En ambos casos los datos son los mismos. La diferencia es lo que dice la clave: el lugar o el producto.

Pruébalo

Arregla la clave

Arregla el carrito para que, tras «Quitar», cada número se quede con su producto. La comprobación escribe 3 para el pan y 2 para la leche, quita el pan y mira los campos de la leche y del queso.

Vue

Pruébalo

Completa el hueco

¿Cuál es el atributo con el que la biblioteca reconoce una fila?

<li v-for="p in items" ="p.id">
  <ProductCard :product="p" />
</li>

La idea

Una buena clave

Una buena clave es estable y única: la misma para el mismo producto en cada dibujado, y distinta para productos distintos. Lo mejor es un id de los datos: viene de la base de datos y no cambia.

  • El índice es una mala clave si la lista cambia: al quitar, al añadir al principio, al ordenar, al filtrar. Sirve si la lista es solo de lectura y nunca cambia de orden.
  • Math.random() es siempre una mala clave. En cada dibujado la clave es nueva, así que ninguna fila vieja tiene pareja. La biblioteca quita todas las filas y hace otras nuevas. Los campos pierden lo escrito, y el foco desaparece.
  • Dos claves iguales confunden el emparejamiento. Aquí la clave es el nombre, y «Yogur» hay en dos tamaños. Pulsa «Ordenar por precio» y cuenta las filas.
Vue

Son cuatro productos, y las filas pasan a ser cinco: un yogur está dos veces.

Vue avisa solo en la actualización: «Duplicate keys found during update: "Yogur"». En el primer dibujado calla. Si te olvidas de :key, Vue no dice nada: compara las filas por lugar. Por eso el linter de Vue (eslint-plugin-vue) pide una clave en cada v-for. Los avisos existen solo en modo de desarrollo, y el error en la página sigue también en el sitio real.

Pruébalo

Encuentra el error

En cada dibujado todos los campos «Cantidad» vuelven a 1, y el cursor sale del campo.

Elige la línea con el error.

La idea

Filtrar y ordenar

El catálogo muestra solo los productos disponibles, ordenados por precio. Es una lista derivada: no se guarda aparte, sino que se hace a partir de los datos en cada dibujado.

const visible = PRODUCTS
.filter((p) => p.stock > 0)
.sort((a, b) => a.price - b.price);

Esto está en <script setup>. Aquí PRODUCTS no cambia, y basta con calcularlo una vez. Para datos que cambian, Vue tiene computed. De eso trata la lección «Estado derivado».

  • filter devuelve un array nuevo solo con los productos disponibles. El original queda entero.
  • sort ordena el array en el sitio. Aquí es seguro, porque va después de filter, que ya dio una copia. Sin filtro, antes haces una copia: [...PRODUCTS].sort(…).
  • La clave sigue siendo p.id. Al ordenar, las filas cambian de lugar, y el índice cambiaría la clave de cada una.

Pruébalo

Solo lo que está disponible

Muestra solo los productos disponibles (stock > 0), ordenados por precio, del más barato al más caro. Encima de la lista escribe «Disponibles: 3 de 5».

Vue
Más precisamente, para curiosos

key no es una prop. La biblioteca se queda con la clave y no se la pasa al componente. defineProps(['key']) da el aviso «"key" is a reserved property». Si el componente necesita el id, pásalo aparte: product ya lo contiene.

Una clave para reiniciar. Una clave nueva es un elemento nuevo. Así que cambiar la clave es una manera de reiniciar un componente: un formulario con el id del producto como clave empieza vacío para cada producto nuevo.

Varios elementos por fila. Si un producto son dos elementos (<dt> y <dd>), v-for y :key van en un <template>: <template v-for="p in items" :key="p.id">.

v-if y v-for en el mismo elemento. En Vue 3 v-if se ejecuta antes que v-for y no ve p. El filtro va en los datos (visible), no en un v-if en el mismo elemento.

Options API. El código antiguo de Vue usa data() y methods en lugar de <script setup>. v-for y :key son los mismos.

toSorted. products.toSorted((a, b) => a.price - b.price) devuelve una copia ordenada y no toca el original. Está en los navegadores desde 2023.

Por qué el campo es «no controlado». Aquí el número del campo está solo en el DOM. En el módulo «Estado» el campo pasa a ser controlado: el número está en los datos y los sigue. Entonces la clave sigue importando: por el foco, el cursor y el estado que el componente guarda dentro de sí mismo.

Ejercicio

El carrito

El estado y el botón «Ordenar por precio» están listos. Haz la fila de cada producto: ProductCard y un botón «Quitar» cuyo nombre para un lector de pantalla sea «Quitar Pan blanco». La comprobación escribe cantidades, ordena, quita el pan y mira que cada número se quede con su producto.

Vue

Test

Ponte a prueba

1. ¿Cómo sabe la biblioteca qué fila de la lista vieja es cada fila de la nueva?
2. La clave es el índice. El comprador quita el primer producto. ¿Qué pasa con los campos de las otras filas?
3. ¿Por qué Math.random() es una mala clave?
4. La lista tiene que estar ordenada por precio. ¿Cómo?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·