Keys y Renderizado de Listas

Hasta ahora hemos trabajado con componentes individuales. En esta clase aprenderemos a renderizar listas de datos dinámicamente usando .map(), y entenderemos por qué React necesita que cada elemento tenga una key única.

El problema: Componentes repetidos manualmente

Imagina que queremos mostrar una lista de trabajos:

function App() {
  return (
    <div>
      <article className="job-listing-card">
        <h3>Frontend Developer</h3>
        <p>TechCorp</p>
        <p>Madrid, España</p>
      </article>

      <article className="job-listing-card">
        <h3>Backend Developer</h3>
        <p>DataStack</p>
        <p>Barcelona, España</p>
      </article>

      <article className="job-listing-card">
        <h3>Full Stack Developer</h3>
        <p>StartupX</p>
        <p>Valencia, España</p>
      </article>
    </div>
  )
}

Problemas:

  • 📝 Repetitivo - Copiar y pegar el mismo HTML
  • 🔢 No escala - ¿Y si tienes 100 trabajos?
  • 📊 Datos hardcodeados - Los datos están mezclados con la UI
  • 🔄 Difícil de actualizar - Cambiar algo requiere tocar múltiples lugares

Creando el archivo de datos

Primero, creemos un archivo JSON con nuestros datos:

// src/data/jobs.json
[
  {
    "id": "7a4d1d8b-1e45-4d8c-9f1a-8c2f9a9121a4",
    "titulo": "Desarrollador de Software Senior",
    "empresa": "Tech Solutions Inc.",
    "ubicacion": "Remoto",
    "descripcion": "Buscamos un ingeniero de software con experiencia en desarrollo web y conocimientos en JavaScript, React y Node.js. El candidato ideal debe ser capaz de trabajar en equipo y tener buenas habilidades de comunicación.",
    "data": {
      "technology": ["react", "node", "javascript"],
      "modalidad": "remoto",
      "nivel": "senior"
    }
  },
  {
    "id": "d35b2c89-5d60-4f26-b19a-6cfb2f1a0f57",
    "titulo": "Analista de Datos",
    "empresa": "Data Driven Co.",
    "ubicacion": "Ciudad de México",
    "descripcion": "Estamos buscando un analista de datos con experiencia en el manejo de grandes conjuntos de datos y herramientas de visualización. Se requiere conocimiento en SQL, Python y R.",
    "data": {
      "technology": "python",
      "modalidad": "cdmx",
      "nivel": "junior"
    }
  }
  // ... más elementos
]

Creando el componente JobCard

Ahora creemos un componente para mostrar cada trabajo:

// src/components/JobCard.jsx
export function JobCard({ job }) {
  return (
    <article
      className="job-listing-card"
      data-modalidad={job.data.modalidad}
      data-nivel={job.data.nivel}
      data-technology={job.data.technology}
    >
      <div>
        <h3>{job.titulo}</h3>
        <small>
          {job.empresa} | {job.ubicacion}
        </small>
        <p>{job.descripcion}</p>
      </div>
      <button>Aplicar</button>
    </article>
  )
}

Puntos clave:

  • Recibe job como prop
  • Accede a las propiedades del objeto: job.title, job.company, etc.

Renderizando la lista con .map()

Ahora creemos el componente que renderiza la lista de trabajos:

// src/components/JobListings.jsx
import { JobCard } from './JobCard'
import jobsData from './data/jobs.json'

export function JobListings() {
  return (
    <div className="jobs-grid">
      {jobsData.map((job) => (
        <JobCard key={job.id} job={job} />
      ))}
    </div>
  )
}

¿Qué hace .map()?

// Array de datos
const jobs = [
  { id: 1, title: 'Frontend Developer' },
  { id: 2, title: 'Backend Developer' },
  { id: 3, title: 'Full Stack Developer' },
]

// .map() transforma cada elemento
jobs.map((job) => <JobCard key={job.id} job={job} />)

// Resultado: Array de componentes React
// [
//   <JobCard key={1} job={{...}} />,
//   <JobCard key={2} job={{...}} />,
//   <JobCard key={3} job={{...}} />
// ]

¿Por qué necesitamos la key?

La prop key es obligatoria cuando renderizas listas. Sin ella, verás este warning:

⚠️ Warning: Each child in a list should have a unique "key" prop.

¿Qué es la key?

La key es un identificador único que ayuda a React a identificar qué elementos cambiaron, se agregaron o se eliminaron.

{
  jobs.map((job) => <JobCard key={job.id} job={job} />)
}
//                           ↑
//                      Obligatoria y única

¿Por qué React necesita keys?

React necesita saber qué elemento es cuál cuando la lista cambia.

Sin keys: React no sabe qué cambió

// Renderizado inicial
[
  <JobCard />,  // Frontend Developer
  <JobCard />,  // Backend Developer
  <JobCard />,  // Full Stack Developer
]

// Agregas uno al inicio
[
  <JobCard />,  // 🆕 DevOps Engineer
  <JobCard />,  // Frontend Developer
  <JobCard />,  // Backend Developer
  <JobCard />,  // Full Stack Developer
]

// React piensa: "¿Todos cambiaron? Re-renderizo todos" 🐌

Con keys: React sabe exactamente qué cambió

// Renderizado inicial
[
  <JobCard key={1} />,  // Frontend Developer
  <JobCard key={2} />,  // Backend Developer
  <JobCard key={3} />,  // Full Stack Developer
]

// Agregas uno al inicio
[
  <JobCard key={4} />,  // 🆕 DevOps Engineer (NUEVO)
  <JobCard key={1} />,  // Frontend Developer (mismo)
  <JobCard key={2} />,  // Backend Developer (mismo)
  <JobCard key={3} />,  // Full Stack Developer (mismo)
]

// React piensa: "Solo key={4} es nuevo, solo renderizo ese" ⚡

Reglas de las keys

1. Deben ser únicas entre hermanos

// ✅ Correcto: IDs únicos
{
  jobs.map((job) => <JobCard key={job.id} job={job} />)
}

// ❌ Error: Todas tienen la misma key
{
  jobs.map((job) => <JobCard key="job" job={job} />)
}

2. Deben ser estables

La key no debería cambiar entre renderizados:

// ❌ Mal: Math.random() genera keys diferentes cada vez
{
  jobs.map((job) => <JobCard key={Math.random()} job={job} />)
}

// ✅ Bien: El ID no cambia
{
  jobs.map((job) => <JobCard key={job.id} job={job} />)
}

3. Preferiblemente, usa IDs del dato

// ✅ Mejor: ID del objeto
{
  jobs.map((job) => <JobCard key={job.id} job={job} />)
}

// ⚠️ Aceptable solo si la lista NUNCA cambia
{
  jobs.map((job, index) => <JobCard key={index} job={job} />)
}

¿Cuándo usar índices como keys?

Los índices del array (index) solo son seguros cuando:

  • ✅ La lista nunca se reordena
  • ✅ La lista nunca cambia (no se agregan/eliminan items)
  • ✅ Los items no tienen un ID único

Ejemplo donde es seguro:

const DAYS_OF_WEEK = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes']

// ✅ OK: Lista estática que nunca cambia
{
  DAYS_OF_WEEK.map((day, index) => <li key={index}>{day}</li>)
}

Ejemplo donde NO es seguro:

const [tasks, setTasks] = useState([...])

// ❌ Mal: Lista dinámica que cambia
{tasks.map((task, index) => (
  <Task key={index} task={task} />
))}

// ✅ Bien: Usa ID único
{tasks.map((task) => (
  <Task key={task.id} task={task} />
))}

Problema con índices: Ejemplo visual

Imagina una lista de tareas:

// Lista inicial
;[
  { id: 'a', text: 'Tarea A', completed: false },
  { id: 'b', text: 'Tarea B', completed: true },
  { id: 'c', text: 'Tarea C', completed: false },
]

Con índices como key:

// Renderizado inicial
<Task key={0} text="Tarea A" completed={false} />
<Task key={1} text="Tarea B" completed={true} />
<Task key={2} text="Tarea C" completed={false} />

// Usuario elimina "Tarea A"
<Task key={0} text="Tarea B" completed={true} />  // ← key cambió!
<Task key={1} text="Tarea C" completed={false} /> // ← key cambió!

// React piensa: "key={0} cambió de 'Tarea A' a 'Tarea B'"
// Puede causar bugs en el estado o animaciones

Con IDs como key:

// Renderizado inicial
<Task key="a" text="Tarea A" completed={false} />
<Task key="b" text="Tarea B" completed={true} />
<Task key="c" text="Tarea C" completed={false} />

// Usuario elimina "Tarea A"
<Task key="b" text="Tarea B" completed={true} />  // ← key no cambió
<Task key="c" text="Tarea C" completed={false} /> // ← key no cambió

// React piensa: "key='a' se eliminó, las demás siguen igual"
// Todo funciona correctamente

Lista vacía: Renderizado condicional

¿Qué pasa si no hay trabajos? Deberíamos mostrar un mensaje:

export function JobListings({ jobs }) {
  if (jobs.length === 0) {
    return (
      <div className="no-jobs">
        <p>No hay trabajos disponibles en este momento.</p>
      </div>
    )
  }

  return (
    <div className="jobs-grid">
      {jobs.map((job) => (
        <JobCard key={job.id} job={job} />
      ))}
    </div>
  )
}

O con operador ternario:

export function JobListings({ jobs }) {
  return (
    <div>
      {jobs.length === 0 ? (
        <p>No hay trabajos disponibles.</p>
      ) : (
        <div className="jobs-grid">
          {jobs.map((job) => (
            <JobCard key={job.id} job={job} />
          ))}
        </div>
      )}
    </div>
  )
}

Lo que hemos visto en esta clase

  • 🗂️ Importar JSON - Cargar datos desde archivos externos
  • 🧩 Componente JobCard - Mostrar un trabajo individual
  • 📋 Componente JobListings - Renderizar lista de trabajos
  • 🔄 .map() - Transformar array de datos en array de componentes
  • 🔑 Key obligatoria - React necesita identificar elementos únicos
  • 🎯 Keys únicas y estables - Usar IDs en lugar de índices
  • ⚠️ Problema con índices - Por qué no usar index en listas dinámicas
  • 📊 Flujo de datos - JSON → JobListings → JobCard
  • 🎭 Renderizado condicional - Mostrar mensaje si lista está vacía

En la próxima clase vamos a terminar de hacer funcionar nuestra paginación.

💡 Recuerda: Siempre usa keys únicas y estables al renderizar listas. Prefiere IDs del objeto sobre índices del array. La key ayuda a React a optimizar los renderizados identificando qué elementos cambiaron.