Skip to content

Condicionales y listas ​

Casi toda interfaz muestra cosas según condiciones (¿hay sesión?, ¿está cargando?, ¿hay resultados?) y repite elementos a partir de datos (productos, mensajes, filas de una tabla). Aprenderás los patrones de renderizado condicional y a renderizar listas correctamente con key.

Concepto ​

En JSX no puedes escribir if ni for dentro de las llaves, pero sí usar sus equivalentes como expresiones:

NecesidadHerramienta
Mostrar A o BTernario condicion ? A : B
Mostrar algo o nadacondicion && A
Varias opcionesif antes del return, objeto de mapeo o switch en una función
Repetir elementosarreglo.map()
Filtrar antes de repetirarreglo.filter().map()

Renderizado condicional ​

if antes del return (retornos tempranos) ​

Ideal para estados completos de la pantalla:

jsx
function ListaPedidos({ cargando, error, pedidos }) {
  if (cargando) return <p>Cargando pedidos...</p>
  if (error) return <p role="alert">Error: {error}</p>
  if (pedidos.length === 0) return <p>Aún no tienes pedidos.</p>

  return (
    <ul>
      {pedidos.map((p) => (
        <li key={p.id}>Pedido #{p.id}</li>
      ))}
    </ul>
  )
}

Ternario: una cosa u otra ​

jsx
function Navbar({ usuario }) {
  return (
    <nav>
      <a href="/">Inicio</a>
      {usuario ? (
        <span>Hola, {usuario.nombre} · <button>Salir</button></span>
      ) : (
        <a href="/login">Iniciar sesión</a>
      )}
    </nav>
  )
}

&&: algo o nada ​

jsx
function Bandeja({ noLeidos }) {
  return (
    <div>
      Mensajes
      {noLeidos > 0 && <span className="badge">{noLeidos}</span>}
    </div>
  )
}

El problema del 0

Si la condición es un número, && puede mostrar un 0 en pantalla:

jsx
{mensajes.length && <Lista />}      // ❌ si length es 0, se muestra "0"
{mensajes.length > 0 && <Lista />}  // ✅ booleano
{!!mensajes.length && <Lista />}    // ✅ también

Objeto de mapeo: varias opciones ​

Más limpio que ternarios anidados:

jsx
const ESTADOS = {
  pendiente: { texto: 'Pendiente', color: '#ca8a04' },
  pagado: { texto: 'Pagado', color: '#16a34a' },
  enviado: { texto: 'Enviado', color: '#2563eb' },
  cancelado: { texto: 'Cancelado', color: '#dc2626' }
}

function EstadoPedido({ estado }) {
  const { texto, color } = ESTADOS[estado] ?? { texto: 'Desconocido', color: 'gray' }
  return <span style={{ color, fontWeight: 600 }}>● {texto}</span>
}

Ocultar vs no renderizar ​

jsx
{/* No renderiza: el componente se destruye y pierde su estado */}
{visible && <Formulario />}

{/* Oculta con CSS: se conserva el estado del formulario */}
<div hidden={!visible}><Formulario /></div>

Listas con map ​

jsx
const frutas = ['Manzana', 'Mango', 'Uva']

function ListaFrutas() {
  return (
    <ul>
      {frutas.map((fruta) => (
        <li key={fruta}>{fruta}</li>
      ))}
    </ul>
  )
}

map transforma cada dato en un elemento JSX. React recibe un arreglo de elementos y los dibuja.

La prop key ​

Cada elemento de una lista necesita una key única y estable entre sus hermanos. React la usa para saber qué elemento es cuál cuando la lista cambia (se agrega, elimina o reordena algo).

Analogía: en un salón, si identificas a los alumnos por su lugar en la fila ("el tercero"), cuando alguien se cambia de lugar todo se confunde. Si los identificas por su número de matrícula, no importa dónde se sienten.

jsx
// ✅ id de la base de datos
{usuarios.map((u) => <Usuario key={u.id} usuario={u} />)}

// ⚠️ índice: sólo si la lista NUNCA se reordena, filtra ni elimina elementos
{pasos.map((paso, i) => <li key={i}>{paso}</li>)}

// ❌ aleatorio: cambia en cada render, React recrea todo
{items.map((it) => <li key={Math.random()}>{it}</li>)}

Error común

Usar el índice como key en listas editables. Si eliminas el primer elemento, React reutiliza los componentes por posición y los inputs o el estado interno aparecen en la fila equivocada.

La key va en el elemento más externo que devuelve el map. Si necesitas devolver varios elementos sin envoltura, usa Fragment con key:

jsx
import { Fragment } from 'react'

function Glosario({ terminos }) {
  return (
    <dl>
      {terminos.map((t) => (
        <Fragment key={t.id}>
          <dt>{t.palabra}</dt>
          <dd>{t.definicion}</dd>
        </Fragment>
      ))}
    </dl>
  )
}

Filtrar, ordenar y agrupar ​

jsx
const productos = [
  { id: 1, nombre: 'Laptop', precio: 15000, categoria: 'Cómputo', stock: 3 },
  { id: 2, nombre: 'Mouse', precio: 350, categoria: 'Cómputo', stock: 0 },
  { id: 3, nombre: 'Silla', precio: 2800, categoria: 'Muebles', stock: 10 },
  { id: 4, nombre: 'Escritorio', precio: 4500, categoria: 'Muebles', stock: 2 }
]

function Catalogo() {
  const disponibles = productos
    .filter((p) => p.stock > 0)                 // sólo con stock
    .toSorted((a, b) => a.precio - b.precio)    // más barato primero (sin mutar)

  // Agrupar por categoría: { Cómputo: [...], Muebles: [...] }
  const porCategoria = Object.groupBy(disponibles, (p) => p.categoria)

  return Object.entries(porCategoria).map(([categoria, lista]) => (
    <section key={categoria}>
      <h2>{categoria}</h2>
      <ul>
        {lista.map((p) => (
          <li key={p.id}>
            {p.nombre} — ${p.precio.toLocaleString('es-MX')}
          </li>
        ))}
      </ul>
    </section>
  ))
}

Caso real: tabla de usuarios con búsqueda y filtro ​

jsx
import { useState } from 'react'

const USUARIOS = [
  { id: 1, nombre: 'Ana Torres', email: 'ana@empresa.com', rol: 'admin', activo: true },
  { id: 2, nombre: 'Luis Pérez', email: 'luis@empresa.com', rol: 'editor', activo: true },
  { id: 3, nombre: 'Marta Ruiz', email: 'marta@empresa.com', rol: 'lector', activo: false },
  { id: 4, nombre: 'Jorge Díaz', email: 'jorge@empresa.com', rol: 'editor', activo: true }
]

export default function TablaUsuarios() {
  const [busqueda, setBusqueda] = useState('')
  const [rol, setRol] = useState('todos')
  const [soloActivos, setSoloActivos] = useState(false)

  const texto = busqueda.trim().toLowerCase()
  const visibles = USUARIOS.filter(
    (u) =>
      (u.nombre.toLowerCase().includes(texto) || u.email.includes(texto)) &&
      (rol === 'todos' || u.rol === rol) &&
      (!soloActivos || u.activo)
  )

  return (
    <div style={{ maxWidth: 720, margin: '2rem auto', fontFamily: 'system-ui' }}>
      <div style={{ display: 'flex', gap: 8, marginBottom: 16, flexWrap: 'wrap' }}>
        <input
          placeholder="Buscar por nombre o email"
          value={busqueda}
          onChange={(e) => setBusqueda(e.target.value)}
        />
        <select value={rol} onChange={(e) => setRol(e.target.value)}>
          <option value="todos">Todos los roles</option>
          <option value="admin">Admin</option>
          <option value="editor">Editor</option>
          <option value="lector">Lector</option>
        </select>
        <label>
          <input type="checkbox" checked={soloActivos} onChange={(e) => setSoloActivos(e.target.checked)} />
          Sólo activos
        </label>
      </div>

      {visibles.length === 0 ? (
        <p>No hay usuarios que coincidan con la búsqueda.</p>
      ) : (
        <table width="100%" cellPadding={8} style={{ borderCollapse: 'collapse' }}>
          <thead>
            <tr style={{ textAlign: 'left', borderBottom: '2px solid #ddd' }}>
              <th>Nombre</th>
              <th>Email</th>
              <th>Rol</th>
              <th>Estado</th>
            </tr>
          </thead>
          <tbody>
            {visibles.map((u) => (
              <tr key={u.id} style={{ borderBottom: '1px solid #eee', opacity: u.activo ? 1 : 0.5 }}>
                <td>{u.nombre}</td>
                <td>{u.email}</td>
                <td>{u.rol}</td>
                <td>{u.activo ? '✅ Activo' : '⛔ Inactivo'}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
      <p>{visibles.length} de {USUARIOS.length} usuarios</p>
    </div>
  )
}

Buena práctica

Si el JSX dentro de un map crece, extráelo a un componente (FilaUsuario) y pon la key en ese componente: <FilaUsuario key={u.id} usuario={u} />.

Ejercicios ​

  1. Crea un componente Semaforo que reciba color (rojo, amarillo, verde) y muestre "Alto", "Precaución" o "Avanza" usando un objeto de mapeo.
  2. Dada una lista de tareas con { id, texto, prioridad: 'alta' | 'media' | 'baja', hecha }, muéstralas agrupadas por prioridad (alta primero), con las hechas tachadas, y un mensaje si no hay tareas.
Ver solución
jsx
// 1
const SEMAFORO = {
  rojo: { texto: 'Alto', fondo: '#dc2626' },
  amarillo: { texto: 'Precaución', fondo: '#eab308' },
  verde: { texto: 'Avanza', fondo: '#16a34a' }
}

export function Semaforo({ color }) {
  const info = SEMAFORO[color]
  if (!info) return <p>Color inválido</p>
  return <div style={{ background: info.fondo, color: 'white', padding: 16 }}>{info.texto}</div>
}
jsx
// 2
const ORDEN = ['alta', 'media', 'baja']

export function TareasPorPrioridad({ tareas }) {
  if (tareas.length === 0) return <p>¡No hay tareas! 🎉</p>

  const grupos = Object.groupBy(tareas, (t) => t.prioridad)

  return ORDEN.filter((p) => grupos[p]?.length > 0).map((prioridad) => (
    <section key={prioridad}>
      <h3>Prioridad {prioridad}</h3>
      <ul>
        {grupos[prioridad].map((t) => (
          <li key={t.id} style={{ textDecoration: t.hecha ? 'line-through' : 'none' }}>
            {t.texto}
          </li>
        ))}
      </ul>
    </section>
  ))
}

Resumen ​

  • Usa retornos tempranos para estados completos, ternarios para A/B y && para algo/nada.
  • Cuidado con 0 && ...: usa condiciones booleanas.
  • Renderiza listas con map y una key única y estable (el id, no el índice si la lista cambia).
  • Filtra y ordena antes de mapear, sin mutar los datos originales.

Academia Web — aprende, copia y construye.