Módulo 2: Jsx And Rendering
La `key` y la identidad estable
Descripción
En la lección 5 renderizaste listas con .map() y aceptaste, como requisito, que cada elemento lleva una key —única, estable, el id del dato—. Esta lección paga esa deuda: te explica qué usa React la key para hacer, por qué debe ser estable, y —lo que de verdad importa— qué exactamente se rompe cuando usas el índice de la lista como key. Y no lo vas a creer de memoria: lo vas a ver ejecutado, con el estado de un checkbox saltando al elemento equivocado.
La key es, probablemente, el concepto de React que más gente usa mal sin darse cuenta —porque el error casi nunca se ve en el momento—. Con datos estáticos, cualquier key "funciona": la pantalla se ve igual. El problema aparece cuando la lista cambia —se reordena, se inserta un elemento, se borra uno— y los elementos tienen estado local (un input a medio escribir, un checkbox marcado, el foco, una animación en curso). Ahí, una key mal elegida hace que ese estado se pegue a la posición equivocada. Como en este módulo aún no hay estado (es el módulo 3), esta lección modela la parte de React que causa el bug —la reconciliación— para que lo veas ahora, y llegues al módulo 3 sabiendo por qué la key importa.
Conexión con el módulo. Esta lección cierra el bloque de listas (5 y 6) y es la más conectada con el resto de la guía: el estado local del que hablamos aquí es el useState del módulo 3, y el bug de la key por índice es uno de los más comunes cuando ese estado convive con listas que cambian (módulos 5 en adelante, cuando el catálogo se filtra en vivo). Entender la key ahora, con el modelo ejecutado, te ahorra horas de "¿por qué el checkmark se movió solo?" después.
Una analogía: las etiquetas del guardarropa
Imagina el guardarropa de un teatro. Llegas, entregas tu abrigo, y te dan una etiqueta con un número. Ese número identifica tu abrigo, no el gancho donde lo colgaron. Da igual si reacomodan los ganchos, si cuelgan abrigos nuevos entre medio, si mueven el tuyo de lugar: cuando vuelves y entregas tu etiqueta, te devuelven tu abrigo, porque la etiqueta sigue al abrigo, no al gancho.
Ahora imagina un guardarropa mal diseñado que, en vez de etiquetar el abrigo, etiqueta el gancho: "tú eres el gancho número 3". Mientras nadie se mueva, funciona. Pero si llega un abrigo nuevo y lo cuelgan en el gancho 1, empujando todos los demás un lugar, el sistema se confunde: sigue pensando que "el gancho 3 es tuyo", pero ahora en el gancho 3 hay otro abrigo. Le devuelves tu etiqueta y te dan el abrigo equivocado —el que quedó en tu vieja posición—.
La key de React es la etiqueta del guardarropa. Si la atas a la identidad del abrigo (el id del producto), sigue al elemento aunque la lista se reordene: React siempre sabe cuál es cuál. Si la atas al gancho (el índice, la posición), se rompe en cuanto la lista cambia: React empareja por posición, y el estado de un elemento termina pegado al que ocupe ese lugar ahora. Guarda la imagen: etiqueta el abrigo (el id), nunca el gancho (el índice).
Qué hace React con la key, exactamente
Para entender el bug hay que entender qué hace React entre un render y el siguiente. React no borra toda la lista y la vuelve a dibujar cada vez —sería lento y perdería cosas como el foco o el texto a medio escribir—. En vez de eso, reconcilia: compara la lista de elementos de antes con la de ahora, y decide qué reutilizar, qué actualizar, qué crear y qué destruir.
Para emparejar "el de antes" con "el de ahora", React usa la key. Un elemento con la misma key en ambos renders se considera el mismo: React reutiliza su instancia y —esto es lo crucial— conserva su estado local. El estado local es todo lo que React maneja por ese elemento y que no viene en las props: el valor de un <input> no controlado, si un checkbox está marcado, cuál tiene el foco, el progreso de una animación. Ese estado no vive en tus datos; vive en el elemento, y React lo sigue por la key.
De ahí sale la regla entera:
- Si la
keyes eliddel dato (estable), un producto conserva sukeyaunque cambie de posición. React lo empareja con su versión anterior, y su estado local lo sigue. Correcto. - Si la
keyes el índice (la posición), el elemento en la posición 0 siempre tienekey0. Cuando insertas algo al frente, el que era 0 ahora es 1, pero React ve "key 0 sigue existiendo" y empareja el estado del viejo 0 con el nuevo elemento que cayó en la posición 0. El estado se queda pegado a la posición, no al dato. Roto.
Vamos a verlo pasar.
Ejemplo trabajado: el estado que salta de elemento
Como aún no tenemos estado de React (módulo 3), modelamos la parte que importa: la reconciliación por key. Imagina una lista de productos donde cada fila tiene un checkbox (por ejemplo, "agregar a favoritos"). Si un checkbox está marcado, eso es estado local —no está en los datos del producto, lo maneja React—. El usuario marca "Wireless Mouse". Luego se inserta un producto nuevo, "Gaming Headset", al frente de la lista. La pregunta: ¿dónde queda la marca?
El modelo replica lo que React hace: empareja el render viejo con el nuevo por key, y conserva el estado local (checked) del elemento cuya key reaparece. Corremos las dos estrategias —key = id y key = índice— sobre exactamente el mismo cambio:
// --- Modelo minimo de la reconciliacion de React POR KEY ---
// React conserva un ESTADO LOCAL por elemento de la lista (aqui: si un checkbox
// esta marcado). Ese estado NO viaja en las props: lo maneja React. Para saber
// que estado corresponde a que elemento entre un render y el siguiente, React
// EMPAREJA por 'key': si una key reaparece, conserva su estado local; si no la
// habia visto, es un elemento nuevo (estado limpio, sin marcar).
function reconcileByKey(oldRows, newProducts, keyOf) {
const oldByKey = new Map(oldRows.map((r) => [r.key, r]));
return newProducts.map((product, index) => {
const key = keyOf(product, index);
const prev = oldByKey.get(key); // el estado viejo con esta misma key
return { key, product, checked: prev ? prev.checked : false };
});
}
function show(rows) {
return rows
.map((r) => ` [${r.checked ? 'x' : ' '}] ${r.product.name} (key=${r.key})`)
.join('\n');
}
const mouse = { id: 'p1', name: 'Wireless Mouse' };
const keyboard = { id: 'p2', name: 'Mechanical Keyboard' };
const headset = { id: 'p3', name: 'Gaming Headset' };
// El usuario ya marco el checkbox de "Wireless Mouse". Ahora se INSERTA un
// producto nuevo ("Gaming Headset") al FRENTE de la lista.
const newProducts = [headset, mouse, keyboard];
// --- Estrategia A: key = product.id (identidad ESTABLE) ---
const oldRowsById = [
{ key: 'p1', product: mouse, checked: true }, // <- el usuario marco el mouse
{ key: 'p2', product: keyboard, checked: false },
];
console.log('=== key = product.id (ESTABLE) ===');
console.log('Antes (el usuario marco "Wireless Mouse"):');
console.log(show(oldRowsById));
console.log('Despues de insertar "Gaming Headset" al frente:');
console.log(show(reconcileByKey(oldRowsById, newProducts, (p) => p.id)));
// --- Estrategia B: key = indice de la lista (SE ROMPE) ---
const oldRowsByIndex = [
{ key: 0, product: mouse, checked: true },
{ key: 1, product: keyboard, checked: false },
];
console.log('\n=== key = indice (SE ROMPE) ===');
console.log('Antes (el usuario marco "Wireless Mouse"):');
console.log(show(oldRowsByIndex));
console.log('Despues de insertar "Gaming Headset" al frente:');
console.log(show(reconcileByKey(oldRowsByIndex, newProducts, (p, i) => i)));
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== key = product.id (ESTABLE) ===
Antes (el usuario marco "Wireless Mouse"):
[x] Wireless Mouse (key=p1)
[ ] Mechanical Keyboard (key=p2)
Despues de insertar "Gaming Headset" al frente:
[ ] Gaming Headset (key=p3)
[x] Wireless Mouse (key=p1)
[ ] Mechanical Keyboard (key=p2)
=== key = indice (SE ROMPE) ===
Antes (el usuario marco "Wireless Mouse"):
[x] Wireless Mouse (key=0)
[ ] Mechanical Keyboard (key=1)
Despues de insertar "Gaming Headset" al frente:
[x] Gaming Headset (key=0)
[ ] Wireless Mouse (key=1)
[ ] Mechanical Keyboard (key=2)
Compara las dos mitades; el bug está a la vista.
Con key = product.id (estable), el estado siguió al producto. Antes, la marca [x] estaba en "Wireless Mouse". Después de insertar "Gaming Headset" al frente, la marca sigue en "Wireless Mouse" —ahora en la segunda fila, porque el producto se movió, pero es la fila correcta—. El nuevo "Gaming Headset" aparece sin marcar ([ ]), como debe ser: es un elemento nuevo, con estado limpio. ¿Por qué funcionó? Porque "Wireless Mouse" conservó su key (p1) al cambiar de posición; React lo emparejó con su versión anterior (la marcada) y su estado local lo siguió. La etiqueta iba pegada al abrigo.
Con key = índice, la marca saltó al elemento equivocado. Antes, la marca [x] estaba en "Wireless Mouse" (que ocupaba la posición 0, key=0). Después de insertar "Gaming Headset" al frente, mira dónde quedó la marca: en "Gaming Headset" —un producto que el usuario nunca tocó—. Y "Wireless Mouse", que sí había marcado, quedó sin marca. ¿Por qué? Porque la key era la posición. "Gaming Headset" cayó en la posición 0, heredando la key=0 —la misma que antes tenía "Wireless Mouse"—. React vio "key=0 sigue existiendo" y le pasó a la nueva fila 0 el estado local del viejo 0: la marca. El estado se quedó pegado a la posición, y como el contenido de esa posición cambió, la marca terminó en el producto equivocado. La etiqueta iba pegada al gancho.
Esto es lo que la doc de React quiere decir con "el índice como key causa bugs". No es teórico: es exactamente esta clase de salto de estado, en checkboxes, inputs, foco. Y es silencioso —la pantalla no "falla", solo muestra el estado en el lugar incorrecto—, por eso es tan traicionero.
Profundización: qué hace una buena key
De todo lo anterior sale el perfil de una key correcta. Debe ser:
- Única entre hermanos. Dos elementos de la misma lista no pueden compartir
key(React no sabría cuál es cuál). No tiene que ser única en toda la app, solo entre los hermanos de esa lista. - Estable en el tiempo. El mismo dato debe tener siempre la misma
key, entre renders. Si lakeycambia de un render a otro para el mismo dato, React cree que es un elemento nuevo, destruye el viejo y su estado, y crea uno limpio. Por eso el índice falla (cambia cuando la lista se reordena) y por esoMath.random()como key es un desastre (cambia en cada render). - Derivada de la identidad del dato. Lo natural es el
idque ya traen tus datos (de la base de datos, de la API):product.id. Si de verdad no hayid, genera uno estable una vez (al crear el dato) y guárdalo con él —no lo calcules en el render—.
Y el perfil de las malas key, con su síntoma:
Key Problema Cuando se rompe
────────────────────── ────────────────────────────── ─────────────────────────────
index no es estable: cambia al al reordenar, insertar
reordenar/insertar/borrar o borrar (con estado local)
Math.random() cambia en CADA render siempre: React recrea todo,
pierde foco y estado cada vez
un campo no unico dos elementos comparten key cuando dos datos coinciden
(p. ej. category) en ese campo
product.id (bueno) unico y estable no se rompe
¿Y cuándo es "seguro" el índice? Casi nunca vale la pena arriesgarse, pero hay un caso acotado: si la lista nunca se reordena, ni se inserta o borra en medio (solo se agrega al final), y los elementos no tienen estado local ni identidad propia, el índice no causa daño. Aun así, la recomendación práctica es simple: usa el id del dato siempre que puedas, y trata el índice como último recurso consciente, no como opción por defecto. La key correcta cuesta lo mismo de escribir que la incorrecta.
Errores comunes
Usar el índice como key. Qué pasa: products.map((p, i) => <ProductCard key={i} ... />). Funciona en pantalla hasta que la lista se reordena, se inserta al frente o se borra en medio, y entonces el estado local (input, checkbox, foco) salta al elemento equivocado —como viste ejecutado—. Por qué pasa: el índice está a la mano, y con datos estáticos no se nota el bug. Cómo detectarlo: al reordenar/filtrar/insertar, un input muestra el texto de otra fila, un checkbox aparece marcado donde no debía, o el foco brinca. Cómo corregirlo: usa una key estable ligada al dato (key={p.id}). El síntoma "el estado se pegó a la posición" es la firma del índice como key.
Math.random() (o Date.now()) como key. Qué pasa: key={Math.random()} para "asegurar unicidad". Genera una key nueva en cada render, así que React cree que todos los elementos son nuevos cada vez: destruye y recrea la lista entera en cada render, perdiendo el foco, el texto a medio escribir y el estado, y de paso matando el rendimiento. Por qué pasa: se confunde "única" con "aleatoria". Cómo detectarlo: los inputs pierden el foco al teclear, las animaciones se reinician, la lista "parpadea". Cómo corregirlo: la key debe ser única y estable —la misma para el mismo dato entre renders—. Usa el id del dato, nunca un valor aleatorio ni la hora.
Olvidar la key (y "callar" la advertencia mal). Qué pasa: se omite la key y React avisa; para callar la advertencia, alguien pone key={i} sin pensar. Por qué pasa: se trata la advertencia como un ruido que hay que silenciar, no como una pregunta ("¿cuál es la identidad de cada elemento?"). Cómo detectarlo: la advertencia desaparece pero el bug del índice queda latente. Cómo corregirlo: la advertencia de la key es una invitación a declarar la identidad de cada elemento; respóndela con el id del dato, no con lo primero que calle el mensaje. Poner una key mala es peor que no tener el hábito, porque esconde el problema.
Ejercicios
Ejercicio 1 — Predice el salto. Una lista [A, B, C] usa key = índice. Cada elemento tiene un input con texto: A tiene "hola", B y C están vacíos. Se borra el elemento A (queda [B, C]). Si React empareja por índice, ¿en qué elemento aparece el texto "hola" después, y por qué? ¿Qué pasaría con key = id?
Ver solución
Con key = índice: antes, A ocupaba la posición 0 (key=0) y su input tenía "hola". Al borrar A, la lista pasa a [B, C]: ahora B ocupa la posición 0 (key=0) y C la 1 (key=1). React empareja por índice: el estado local del viejo key=0 (el input con "hola") se conserva para el nuevo elemento en key=0, que ahora es B. Resultado: el texto "hola" aparece en el input de B, aunque el usuario lo escribió en A. El estado se quedó pegado a la posición 0.
Con key = id: A tenía key='a', B key='b', C key='c'. Al borrar A, React ve que key='a' desapareció —destruye ese elemento y su estado ("hola" se va con él, que es lo correcto: A ya no existe)— y que key='b' y key='c' siguen, con su estado intacto (vacíos). B y C quedan vacíos, como deben. La identidad siguió al dato.
Ejercicio 2 — Diagnostica la key. Para cada key, di si es buena o mala y por qué: (a) key={product.id}; (b) key={index}; (c) key={Math.random()}; (d) key={product.category} (varios productos comparten categoría); (e) key={product.name} (los nombres son únicos y no cambian).
Ver solución
- (a)
product.id— Buena. Única y estable, ligada a la identidad del dato. La opción por defecto. - (b)
index— Mala (en general). No es estable: cambia al reordenar, insertar o borrar; causa el salto de estado que viste. Solo tolerable en listas que jamás cambian de orden y sin estado local. - (c)
Math.random()— Muy mala. Cambia en cada render, así que React recrea toda la lista cada vez: pierde foco y estado, y arruina el rendimiento. Nunca. - (d)
product.category— Mala. No es única: si dos productos comparten categoría, compartenkey, y React no sabe distinguirlos. Laskeydeben ser únicas entre hermanos. - (e)
product.name— Aceptable con reservas. Es única y estable si garantizas que los nombres nunca se repiten ni cambian. Pero los nombres suelen editarse o repetirse; elides más seguro. Prefiereidcuando exista.
Ejercicio 3 — Explica el mecanismo. En dos o tres frases, y usando las palabras "emparejar", "estado local" e "identidad", explica por qué React necesita una key estable. Apóyate en la salida del ejemplo trabajado.
Ver solución
React no re-dibuja la lista desde cero en cada render: empareja los elementos de antes con los de ahora para reutilizarlos y conservar su estado local (el checkbox marcado, el texto de un input, el foco), que no vive en los datos. Ese emparejamiento lo hace por la key, así que la key debe representar la identidad del dato y ser estable en el tiempo: si es el id del producto, el estado sigue al producto aunque se mueva (en la salida, la marca se quedó en "Wireless Mouse"); si es el índice, el estado se pega a la posición y salta al elemento que ocupe ese lugar (la marca saltó a "Gaming Headset", recién insertado). Una key estable es cómo React sabe "quién es quién" entre un render y el siguiente.
Resumen y siguiente paso
En esta lección pagaste la deuda de la key y la entendiste de raíz. La key es la identidad que React usa para emparejar cada elemento de una lista entre un render y el siguiente, y así conservar su estado local —el texto de un input, un checkbox marcado, el foco— que no vive en los datos. Por eso debe ser única y estable, ligada al id del dato. Y viste, ejecutado, qué se rompe con el índice: al insertar "Gaming Headset" al frente, con key = id la marca se quedó correctamente en "Wireless Mouse", pero con key = índice saltó al producto recién insertado, porque el estado se pegó a la posición y no al dato. Es la etiqueta del guardarropa: pégala al abrigo (el id), nunca al gancho (el índice).
Antes de avanzar deberías poder: explicar qué hace React con la key (emparejar y conservar estado local); enunciar el perfil de una buena key (única, estable, del dato) y diagnosticar por qué el índice, Math.random() y un campo no único fallan; y describir el bug del índice con el ejemplo del estado que salta.
La lección 7 sube un nivel y recoge el hilo que ha recorrido todo el módulo: React es declarativo. No manipulas el DOM a mano —no "agregas" el badge ni "quitas" una tarjeta—: describes cómo se ve la UI completa para los datos actuales, y React reconcilia la diferencia (con la key, entre otras cosas). Vas a ver, ejecutado, el mismo componente re-descrito para dos estados del dato —un producto que pasa de en stock a agotado— sin tocar ninguna pantalla ni mutar el dato, y a contrastarlo con la secuencia de pasos imperativa que reemplaza. Ahí, todo el módulo se cierra en una sola idea.
Recursos
- React, "Rendering Lists" — sección "Keeping list items in order with key" — react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key. La explicación oficial de la
key: por qué debe ser estable y por qué el índice causa bugs. El centro de esta lección. En inglés. - React, "Preserving and Resetting State" — react.dev/learn/preserving-and-resetting-state. Cómo la posición en el árbol y la
keydeterminan cuándo React conserva o reinicia el estado local; profundiza el mecanismo que modelamos. En inglés. - React, "You Might Not Need an Effect" — react.dev/learn/you-might-not-need-an-effect. Relacionado: el estado derivado y la identidad; útil de fondo para el módulo 5. En inglés.
- MDN, "Map" — developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map. La estructura
Mapde JavaScript que usamos para emparejar porkeyen el modelo de reconciliación. En inglés.