Listas y keys en React: identidad, reconciliación y rendimiento

Capítulo 7

Tiempo estimado de lectura: 9 minutos

+ Ejercicio

Renderizar colecciones con map: del array a la UI

En React, la forma más común de pintar una lista es transformar un array de datos en un array de elementos React usando map. La idea es que cada elemento renderizado represente un ítem de tu colección.

const products = [
  { id: 'p1', name: 'Café' },
  { id: 'p2', name: 'Té' },
];

function ProductList() {
  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

Fíjate en key: es un atributo especial que React usa para identificar de forma estable cada elemento de la lista entre renders.

Qué es key y por qué importa: identidad, reconciliación y rendimiento

Cuando el estado cambia, React vuelve a renderizar y luego compara el árbol anterior con el nuevo para aplicar el mínimo de cambios al DOM (proceso conocido como reconciliación). En listas, React necesita saber qué elemento “nuevo” corresponde a cuál “viejo”.

  • key es identidad: le dice a React “este elemento es el mismo ítem lógico que antes”.
  • Sin una identidad estable, React puede reutilizar nodos/instancias equivocadas, mezclando estado interno (por ejemplo, el valor de un input no controlado o un estado visual local).
  • Con keys correctas, React puede mover, insertar o eliminar ítems con menos trabajo y sin efectos secundarios extraños.

Regla práctica: la key debe ser única entre hermanos y estable en el tiempo para ese ítem.

Por qué NO usar índices como key en listas dinámicas

Usar el índice (key={index}) parece funcionar al principio, pero falla cuando la lista cambia por inserción, eliminación o reordenamiento. El índice no identifica al ítem: identifica su posición. Si la posición cambia, la identidad cambia, y React “cree” que son otros elementos.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

Ejemplo 1: inserción al inicio y inputs que “cambian de fila”

Este ejemplo usa inputs no controlados (sin value), donde el DOM mantiene el texto escrito. Con keys incorrectas, React puede reutilizar el nodo del input para otro ítem.

import { useState } from 'react';

const initial = [
  { id: 'a', label: 'Ana' },
  { id: 'b', label: 'Beto' },
  { id: 'c', label: 'Carla' },
];

export function BadKeys_Insert() {
  const [people, setPeople] = useState(initial);

  function addAtTop() {
    const newPerson = { id: crypto.randomUUID(), label: 'Nueva' };
    setPeople([newPerson, ...people]);
  }

  return (
    <div>
      <button onClick={addAtTop}>Insertar arriba</button>
      <ul>
        {people.map((p, index) => (
          <li key={index}>
            <span>{p.label}: </span>
            <input placeholder="Escribe algo" />
          </li>
        ))}
      </ul>
    </div>
  );
}

Paso a paso para reproducir el problema:

  • Escribe “hola” en el input de “Beto”.
  • Pulsa “Insertar arriba”.
  • Observa que “hola” puede aparecer ahora en otra fila (porque el input DOM se reutilizó para el ítem que quedó en esa posición).

Arreglo: usar una key estable basada en el id del ítem.

{people.map((p) => (
  <li key={p.id}>
    <span>{p.label}: </span>
    <input placeholder="Escribe algo" />
  </li>
))}

Ejemplo 2: eliminación y estado visual que se mezcla

Imagina que cada fila tiene un estado visual local (por ejemplo, “expandido/colapsado”). Si eliminas un elemento en medio y usas índices como key, ese estado puede “saltar” a la fila siguiente.

import { useState } from 'react';

function Row({ label }) {
  const [expanded, setExpanded] = useState(false);

  return (
    <div style={{ padding: 8, border: '1px solid #ddd' }}>
      <button onClick={() => setExpanded((v) => !v)}>
        {expanded ? 'Contraer' : 'Expandir'}
      </button>
      <strong style={{ marginLeft: 8 }}>{label}</strong>
      {expanded && <div>Detalles de {label}</div>}
    </div>
  );
}

export function BadKeys_Delete() {
  const [items, setItems] = useState([
    { id: '1', label: 'Fila 1' },
    { id: '2', label: 'Fila 2' },
    { id: '3', label: 'Fila 3' },
  ]);

  function removeSecond() {
    setItems((prev) => prev.filter((x) => x.id !== '2'));
  }

  return (
    <div>
      <button onClick={removeSecond}>Eliminar Fila 2</button>
      <div style={{ display: 'grid', gap: 8, marginTop: 12 }}>
        {items.map((it, index) => (
          <Row key={index} label={it.label} />
        ))}
      </div>
    </div>
  );
}

Paso a paso:

  • Expande “Fila 3”.
  • Elimina “Fila 2”.
  • Es posible que ahora “Fila 3” pierda/mezcle su estado, o que el estado expandido aparezca asociado a otra fila, porque React reasignó instancias según posición.

Arreglo: key por id.

{items.map((it) => (
  <Row key={it.id} label={it.label} />
))}

Ejemplo 3: reordenamiento y selección que apunta al ítem equivocado

En listas reordenables (por ejemplo, ordenar alfabéticamente), el índice como key hace que React “pegue” el componente a la posición, no al ítem. Si el componente tiene estado local o contiene inputs no controlados, verás inconsistencias.

import { useMemo, useState } from 'react';

function SortableRow({ item, selected, onToggle }) {
  return (
    <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
      <input type="checkbox" checked={selected} onChange={() => onToggle(item.id)} />
      <span>{item.label}</span>
    </div>
  );
}

export function BadKeys_Reorder() {
  const [items, setItems] = useState([
    { id: 'a', label: 'Zeta' },
    { id: 'b', label: 'Alfa' },
    { id: 'c', label: 'Beta' },
  ]);
  const [selectedIds, setSelectedIds] = useState(() => new Set());

  const sorted = useMemo(() => {
    return [...items].sort((x, y) => x.label.localeCompare(y.label));
  }, [items]);

  function toggle(id) {
    setSelectedIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }

  function shuffle() {
    setItems((prev) => [...prev].reverse());
  }

  return (
    <div>
      <button onClick={shuffle}>Reordenar</button>
      <div style={{ display: 'grid', gap: 8, marginTop: 12 }}>
        {sorted.map((item, index) => (
          <SortableRow
            key={index}
            item={item}
            selected={selectedIds.has(item.id)}
            onToggle={toggle}
          />
        ))}
      </div>
    </div>
  );
}

Aunque aquí el checkbox es controlado (depende de selectedIds), en cuanto añadas estado local por fila (hover persistente, edición inline con input no controlado, animaciones, etc.), el reordenamiento con keys por índice tenderá a producir “traspasos” de estado entre filas.

Guía práctica: cómo elegir buenas keys

Paso 1: identifica el “id” real del dominio

La mejor key suele ser un identificador persistente del dato: product.id, userId, slug, etc. Debe mantenerse igual aunque cambie el orden.

  • Bien: key={item.id}
  • Bien: key={item.slug} si es único y estable
  • Mal: key={index} si la lista puede cambiar
  • Mal: key={Math.random()} o key={crypto.randomUUID()} generado en render (cambia en cada render, fuerza remount y destruye estado)

Paso 2: si no tienes id, créalo al ingresar los datos (no al renderizar)

Si tus ítems vienen sin id (por ejemplo, una lista de tareas creada en el cliente), asigna un id al momento de crear el ítem y guárdalo en el estado.

import { useState } from 'react';

export function TodoList() {
  const [todos, setTodos] = useState([]);
  const [text, setText] = useState('');

  function addTodo(e) {
    e.preventDefault();
    const trimmed = text.trim();
    if (!trimmed) return;

    setTodos((prev) => [
      ...prev,
      { id: crypto.randomUUID(), text: trimmed, done: false },
    ]);
    setText('');
  }

  return (
    <div>
      <form onSubmit={addTodo}>
        <input value={text} onChange={(e) => setText(e.target.value)} />
        <button>Añadir</button>
      </form>

      <ul>
        {todos.map((t) => (
          <li key={t.id}>{t.text}</li>
        ))}
      </ul>
    </div>
  );
}

Paso 3: en listas anidadas, la key debe ser única en su nivel

La unicidad de key se evalúa entre hermanos del mismo array. Si renderizas sublistas, cada sublista necesita sus propias keys.

{groups.map((g) => (
  <section key={g.id}>
    <h3>{g.name}</h3>
    <ul>
      {g.items.map((it) => (
        <li key={it.id}>{it.label}</li>
      ))}
    </ul>
  </section>
))}

Paso 4: cuándo el índice puede ser aceptable

Solo considera key={index} si se cumplen todas estas condiciones:

  • La lista es estática (no se inserta, elimina ni reordena).
  • Los ítems no tienen estado local ni contienen inputs no controlados.
  • No hay animaciones/transiciones por ítem que dependan de montaje/desmontaje.

En la práctica, muchas listas terminan siendo dinámicas, así que es más seguro diseñar con ids desde el inicio.

Estructurar componentes de ítem: separar presentación y lógica de selección

Además de elegir buenas keys, una lista suele necesitar interacción (seleccionar, marcar, editar). Una forma mantenible es separar:

  • Componente de presentación: recibe datos y callbacks, no decide reglas.
  • Contenedor/lista: mantiene la estructura de datos (por ejemplo, ids seleccionados) y define la lógica.

Ejemplo: lista seleccionable con keys estables

function ItemRowView({ label, selected, onToggle }) {
  return (
    <div
      onClick={onToggle}
      style={{
        padding: 10,
        border: '1px solid #ddd',
        background: selected ? '#e8f0ff' : 'white',
        cursor: 'pointer',
        display: 'flex',
        justifyContent: 'space-between',
      }}
      role="button"
      tabIndex={0}
    >
      <span>{label}</span>
      <span>{selected ? 'Seleccionado' : ''}</span>
    </div>
  );
}

export function SelectableList({ items }) {
  const [selectedIds, setSelectedIds] = useState(() => new Set());

  function toggle(id) {
    setSelectedIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }

  return (
    <div style={{ display: 'grid', gap: 8 }}>
      {items.map((it) => (
        <ItemRowView
          key={it.id}
          label={it.label}
          selected={selectedIds.has(it.id)}
          onToggle={() => toggle(it.id)}
        />
      ))}
    </div>
  );
}

Ventajas de esta estructura:

  • La identidad visual de cada fila queda anclada a it.id (key estable).
  • La selección se modela por ids, así que sobrevive a reordenamientos e inserciones.
  • ItemRowView es fácil de reutilizar y testear: solo pinta según props.

Checklist rápido para depurar problemas de listas

SíntomaCausa probableQué revisar
Texto escrito en un input aparece en otra fila tras insertar/eliminarKeys por índice o keys inestablesUsa key={item.id} y evita generar keys en render
Filas “reciclan” estado (expandido, hover persistente, edición)Identidad ligada a posiciónKey estable por ítem; evita index en listas dinámicas
Rendimiento pobre en listas grandes al reordenarReact no puede reconciliar eficientementeKeys estables y componentes de ítem bien delimitados
Animaciones se reinician o parpadeanRemount por key cambianteNo uses Math.random()/UUID en render

Ahora responde el ejercicio sobre el contenido:

En una lista dinámica en React donde se insertan o eliminan elementos, ¿qué práctica ayuda a evitar que se mezclen estados locales o que inputs no controlados “cambien de fila” tras un re-render?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

React usa key para asociar cada elemento con su “identidad” durante la reconciliación. En listas que cambian, una key estable y única (como item.id) evita reutilizaciones incorrectas de nodos/instancias y la mezcla de estado o valores de inputs.

Siguiente capítulo

Hooks esenciales en React: useState para estado local y useEffect para sincronización

Arrow Right Icon
Portada de libro electrónico gratuitaReact para principiantes: mentalidad de componentes y manejo de estado
58%

React para principiantes: mentalidad de componentes y manejo de estado

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.