Tema
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:
| Necesidad | Herramienta |
|---|---|
| Mostrar A o B | Ternario condicion ? A : B |
| Mostrar algo o nada | condicion && A |
| Varias opciones | if antes del return, objeto de mapeo o switch en una función |
| Repetir elementos | arreglo.map() |
| Filtrar antes de repetir | arreglo.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énObjeto 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
- Crea un componente
Semaforoque recibacolor(rojo,amarillo,verde) y muestre "Alto", "Precaución" o "Avanza" usando un objeto de mapeo. - 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
&¶ algo/nada. - Cuidado con
0 && ...: usa condiciones booleanas. - Renderiza listas con
mapy unakeyúnica y estable (el id, no el índice si la lista cambia). - Filtra y ordena antes de mapear, sin mutar los datos originales.