React
Listas y keys
Explica cómo renderizar colecciones con map y asignar keys estables para preservar identidad, estado y reconciliación correcta entre elementos.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
React
Explica cómo renderizar colecciones con map y asignar keys estables para preservar identidad, estado y reconciliación correcta entre elementos.
Renderizar una lista no consiste únicamente en ejecutar map. React necesita una identidad estable para relacionar cada elemento anterior con su siguiente versión. Esa identidad se expresa mediante key y determina qué estado, DOM y efectos pertenecen a cada dato.
En JavaScript transformamos colecciones con métodos como map, filter y sort. React utiliza el resultado de esas transformaciones para construir una colección de elementos:
type Product = {
id: string;
name: string;
price: number;
};
function ProductList({ products }: { products: Product[] }) {
return (
<ul>
{products.map((product) => (
<li key={product.id}>
<strong>{product.name}</strong>
<span>{product.price}</span>
</li>
))}
</ul>
);
}map produce un elemento por cada producto. La key no controla el orden ni se muestra en el DOM: comunica qué elemento representa a qué producto dentro de esa colección.
Supón que React renderizó:
[Ana, Bruno, Camila]Después insertas a Diego al principio:
[Diego, Ana, Bruno, Camila]Sin identidad explícita, React solo puede razonar principalmente por posición:
posición 0: Ana → Diego
posición 1: Bruno → Ana
posición 2: Camila→ Bruno
posición 3: nuevo → CamilaCon IDs estables, React puede reconocer movimientos:
Diego → nueva identidad
Ana → misma identidad, nueva posición
Bruno → misma identidad, nueva posición
Camila → misma identidad, nueva posiciónEsto importa porque un elemento puede conservar:
Una key incorrecta puede asociar esas cosas con el dato equivocado.
Dentro de una colección, React relaciona cada hijo mediante:
parent collection
↓
key + element type
↓
component identity
↓
state, refs and DOM preserved or replacedLa key es local al grupo de hermanos. No necesita ser única en toda la aplicación:
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
<ul>
{featuredProducts.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>El mismo ID puede aparecer en listas distintas porque cada colección establece su propio espacio de identidad.
La mejor key proviene de la identidad real del dato:
<OrderRow key={order.id} order={order} />Fuentes adecuadas:
<PermissionRow
key={`${permission.roleId}:${permission.resourceId}`}
permission={permission}
/>No generes la identidad mientras renderizas. Si el dato todavía no tiene ID, créalo cuando nace:
function createDraft(title: string): Draft {
return {
id: crypto.randomUUID(),
title,
};
}items.map((item, index) => (
<EditableRow key={index} item={item} />
));El índice identifica una posición, no una entidad. Si la colección se inserta, elimina, filtra o reordena, el mismo índice representa otro dato.
function EditableRow({ item }: { item: Item }) {
const [name, setName] = useState(item.name);
return (
<input
value={name}
onChange={(event) => setName(event.target.value)}
/>
);
}Con key={index}:
El bug parece una actualización incorrecta de state, pero la causa real es identidad equivocada.
El índice puede funcionar cuando se cumplen todas estas condiciones:
Ejemplo razonable:
const lines = ["Dirección", "Ciudad", "País"];
return (
<ul>
{lines.map((line, index) => (
<li key={index}>{line}</li>
))}
</ul>
);Aun así, si existe una identidad semántica estable, suele ser más expresiva:
<li key={line}>{line}</li><Row key={Math.random()} item={item} />Cada render genera una key diferente:
render 1: key 0.12
render 2: key 0.83React interpreta que el componente anterior desapareció y apareció uno nuevo. Como consecuencia:
Date.now() tiene el mismo problema.
Incorrecto:
function ProductItem({ product }: { product: Product }) {
return <li key={product.id}>{product.name}</li>;
}
products.map((product) => <ProductItem product={product} />);La colección está formada por ProductItem, por lo que la key debe estar allí:
products.map((product) => (
<ProductItem key={product.id} product={product} />
));React necesita identificar los hijos directos producidos por el map.
key es información reservada para React:
<ProductRow key={product.id} product={product} />Dentro de ProductRow, props.key no existe. Si el componente necesita el ID, pásalo como parte del dato o como prop explícita:
<ProductRow
key={product.id}
productId={product.id}
product={product}
/>La sintaxis corta de fragment no acepta key:
entries.map((entry) => (
<>
<dt>{entry.term}</dt>
<dd>{entry.definition}</dd>
</>
));Usa Fragment explícito:
import { Fragment } from "react";
function Glossary({ entries }: { entries: Entry[] }) {
return (
<dl>
{entries.map((entry) => (
<Fragment key={entry.id}>
<dt>{entry.term}</dt>
<dd>{entry.definition}</dd>
</Fragment>
))}
</dl>
);
}La key representa la pareja completa.
Puedes combinar transformaciones antes del JSX:
function ProductList({ products, query }: Props) {
const normalizedQuery = query.trim().toLowerCase();
const visibleProducts = products
.filter((product) =>
product.name.toLowerCase().includes(normalizedQuery),
)
.toSorted((a, b) => a.name.localeCompare(b.name));
return (
<ul>
{visibleProducts.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}Evita mutar una prop con products.sort(...). Métodos mutables pueden alterar el array compartido. Usa toSorted cuando esté soportado o copia antes:
const sortedProducts = [...products].sort(compareProducts);La identidad del dato sigue siendo product.id, aunque cambie su posición visual.
Una lista completa no solo contempla “hay datos”:
function OrdersView({ state }: { state: OrdersState }) {
if (state.status === "loading") {
return <p>Cargando pedidos…</p>;
}
if (state.status === "error") {
return <p role="alert">{state.message}</p>;
}
if (state.orders.length === 0) {
return <p>No hay pedidos todavía.</p>;
}
return (
<ul>
{state.orders.map((order) => (
<li key={order.id}>{order.number}</li>
))}
</ul>
);
}Distingue:
Dos hermanos no deben compartir la misma key:
Warning: Encountered two children with the same keyCuando existen IDs duplicados, no ocultes el problema agregando el índice sin analizarlo. Puede indicar:
Corrige la fuente o construye una identidad de dominio válida.
Una key también puede expresar que un componente representa una entidad nueva fuera de una lista:
<ProfileForm key={user.id} user={user} />Al cambiar el usuario, React reinicia el estado local del formulario. Esto es útil cuando el borrador no debe transferirse entre entidades.
No cambies keys para “forzar un render”. Un setter ya solicita render. Cambiar la key fuerza un remount y destruye estado.
Keys correctas ayudan a preservar identidad, pero no solucionan por sí solas una colección enorme. Para miles de filas evalúa:
La virtualización también debe conservar accesibilidad, navegación por teclado y posición de scroll.
El estado puede desplazarse a otro dato al reordenar o eliminar.
Causa remounts en cada actualización.
React no puede identificar correctamente el elemento directo de la colección.
Un nombre, fecha o categoría puede repetirse aunque parezca estable.
La key pertenece al algoritmo de React, no al contrato público del componente.
Puede ocultar un problema de ownership o sincronización destruyendo todo el estado.
Una lista sin elementos no siempre es un error; debe tener una experiencia explícita.
map transforma datos en elementos; key comunica identidad.key no llega como prop.key={index} puede mover el estado de un input a otra fila?map produce un componente propio?map.Render, commit y actualización del DOM explica qué ocurre después de producir el árbol y por qué una key afecta la preservación de instancias sin significar que todo el DOM se reconstruya.