Listas y renderizado condicional
Las aplicaciones reales muestran colecciones de datos: una lista de productos, un historial de pedidos, resultados de búsqueda. Y no siempre quieres mostrar todo — a veces necesitas ocultar o mostrar elementos según una condición. En esta lección aprenderás ambas cosas: recorrer arrays con map y controlar qué se muestra con renderizado condicional.
Renderizar listas con map
En JavaScript aprendiste que map transforma cada elemento de un array en algo nuevo. En React, usas map para transformar un array de datos en un array de JSX:
function ListaCafes() {
const cafes = ['Café Nebulosa', 'Latte Cósmico', 'Mocha Intergaláctico']
return (
<ul>
{cafes.map(cafe => (
<li key={cafe}>{cafe}</li>
))}
</ul>
)
}
export default ListaCafes
Cada elemento dentro de map necesita una prop key. Es un identificador único que React usa internamente para saber qué elementos cambiaron, se añadieron o se eliminaron. Sin keys, React tendría que re-renderizar toda la lista cada vez que algo cambia.
La prop key: por qué importa
// ❌ MAL: usar el índice como key
{cafes.map((cafe, index) => (
<li key={index}>{cafe}</li>
))}
// ✅ BIEN: usar un identificador único
{cafes.map(cafe => (
<li key={cafe.id}>{cafe.nombre}</li>
))}
Usar el índice como key funciona para listas estáticas, pero causa problemas cuando la lista cambia (elementos se añaden, eliminan o reordenan). La key debe ser un valor estable y único — normalmente el id del dato.
Listas con datos complejos
import { useState } from 'react'
const cafesIniciales = [
{ id: 1, nombre: 'Café Nebulosa', precio: 3.5, disponible: true },
{ id: 2, nombre: 'Latte Cósmico', precio: 4.2, disponible: true },
{ id: 3, nombre: 'Mocha Supernova', precio: 4.8, disponible: false },
{ id: 4, nombre: 'Té Estelar', precio: 3.0, disponible: true },
]
function MenuCafe() {
const [cafes, setCafes] = useState(cafesIniciales)
function toggleDisponible(id) {
setCafes(prev =>
prev.map(cafe =>
cafe.id === id
? { ...cafe, disponible: !cafe.disponible }
: cafe
)
)
}
function eliminar(id) {
setCafes(prev => prev.filter(cafe => cafe.id !== id))
}
return (
<div>
<h2>Menú de Café Estelar</h2>
{cafes.map(cafe => (
<div key={cafe.id} style={{ opacity: cafe.disponible ? 1 : 0.5 }}>
<h3>{cafe.nombre}</h3>
<p>{cafe.precio.toFixed(2)} €</p>
<p>{cafe.disponible ? 'Disponible' : 'Agotado'}</p>
<button onClick={() => toggleDisponible(cafe.id)}>
{cafe.disponible ? 'Marcar agotado' : 'Marcar disponible'}
</button>
<button onClick={() => eliminar(cafe.id)}>Eliminar</button>
</div>
))}
</div>
)
}
export default MenuCafe
Fíjate en cómo usamos map para modificar un elemento específico (el que tiene el id correcto) y filter para eliminar. Siempre creando arrays nuevos — nunca mutando el original.
Renderizado condicional
En React hay varias formas de mostrar u ocultar elementos según una condición:
1. Operador && (AND lógico)
Si la condición es true, renderiza el elemento. Si es false, no renderiza nada:
{pedidos > 0 && <p>Tienes {pedidos} pedidos pendientes</p>}
{usuario && <p>Hola, {usuario.nombre}</p>}
{errores.length > 0 && (
<ul>
{errores.map(err => <li key={err}>{err}</li>)}
</ul>
)}
Cuidado con el 0:
{0 &&renderizaTexto
}0, no nada. Para números, usa una comparación explícita:{count > 0 && ...}.
2. Operador ternario
Cuando necesitas mostrar una cosa u otra según una condición:
{logueado
? <p>Bienvenido, {nombre}</p>
: <p>Inicia sesión para continuar</p>
}
{loading
? <p>Cargando...</p>
: <ListaProductos productos={productos} />
}
3. Return anticipado
Para casos donde toda la vista cambia según una condición:
function PedidoEstado({ pedido }) {
if (!pedido) {
return <p>No hay pedido seleccionado.</p>
}
return (
<div>
<h2>{pedido.nombre}</h2>
<p>Estado: {pedido.estado}</p>
</div>
)
}
Patrón loading / error / data
Un patrón que vas a usar constantemente es manejar tres estados: cargando, error y datos listos:
import { useState } from 'react'
function ListaConEstados() {
const [cafes, setCafes] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
// Simular carga de datos
function cargarCafes() {
setLoading(true)
setError(null)
setTimeout(() => {
// Simular éxito o error aleatorio
if (Math.random() > 0.3) {
setCafes([
{ id: 1, nombre: 'Café Nebulosa' },
{ id: 2, nombre: 'Latte Cósmico' },
])
} else {
setError('Error al cargar los cafés')
}
setLoading(false)
}, 1000)
}
return (
<div>
<button onClick={cargarCafes}>Cargar menú</button>
{loading && <p>Cargando...</p>}
{error && <p style={{ color: 'red' }}>{error}</p>}
{!loading && !error && cafes.length > 0 && (
<ul>
{cafes.map(cafe => (
<li key={cafe.id}>{cafe.nombre}</li>
))}
</ul>
)}
{!loading && !error && cafes.length === 0 && (
<p>No hay cafés para mostrar.</p>
)}
</div>
)
}
export default ListaConEstados
Filtrar y ordenar listas
Puedes combinar filter y sort con map para mostrar listas filtradas sin modificar el estado original:
import { useState } from 'react'
const todosLosCafes = [
{ id: 1, nombre: 'Café Nebulosa', precio: 3.5, tipo: 'espresso' },
{ id: 2, nombre: 'Latte Cósmico', precio: 4.2, tipo: 'latte' },
{ id: 3, nombre: 'Mocha Supernova', precio: 4.8, tipo: 'mocha' },
{ id: 4, nombre: 'Espresso Estelar', precio: 2.5, tipo: 'espresso' },
{ id: 5, nombre: 'Latte Lunar', precio: 3.8, tipo: 'latte' },
]
function MenuFiltrable() {
const [filtro, setFiltro] = useState('todos')
const [busqueda, setBusqueda] = useState('')
// Filtrar y buscar sin modificar el array original
const cafesFiltrados = todosLosCafes
.filter(cafe => filtro === 'todos' || cafe.tipo === filtro)
.filter(cafe => cafe.nombre.toLowerCase().includes(busqueda.toLowerCase()))
.sort((a, b) => a.precio - b.precio)
return (
<div>
<input
value={busqueda}
onChange={(e) => setBusqueda(e.target.value)}
placeholder="Buscar café..."
/>
<div>
{['todos', 'espresso', 'latte', 'mocha'].map(tipo => (
<button
key={tipo}
onClick={() => setFiltro(tipo)}
style={{ fontWeight: filtro === tipo ? 'bold' : 'normal' }}
>
{tipo.charAt(0).toUpperCase() + tipo.slice(1)}
</button>
))}
</div>
<p>{cafesFiltrados.length} resultado(s)</p>
{cafesFiltrados.length === 0
? <p>No se encontraron cafés.</p>
: (
<ul>
{cafesFiltrados.map(cafe => (
<li key={cafe.id}>
{cafe.nombre} — {cafe.precio.toFixed(2)} €
</li>
))}
</ul>
)
}
</div>
)
}
export default MenuFiltrable
Fíjate: los filtros y la búsqueda son estado, pero la lista filtrada es estado derivado — se calcula directamente en el render sin necesidad de un useState extra.
Resumen
maptransforma un array de datos en un array de elementos JSX para renderizar listas.- Cada elemento en una lista necesita una prop key única y estable — preferiblemente un
id, nunca el índice si la lista puede cambiar. - Renderizado condicional con
&&(mostrar si true), ternario (esto o aquello), o return anticipado (para toda la vista). - El patrón loading/error/data maneja los tres estados posibles de una operación asíncrona.
filterysortcombinados conmappermiten mostrar listas filtradas sin mutar el estado original.
En la siguiente lección aprenderás a consumir una API real con fetch y useEffect — aplicando el patrón loading/error/data con datos de verdad.
Gestor de tareas espaciales
Crea un gestor de tareas para misiones espaciales con las siguientes funcionalidades:
- Un formulario para añadir tareas con
nombre,prioridad(alta, media, baja) ycompletada(por defectofalse). Genera unidúnico conDate.now(). - Muestra la lista de tareas con
map, usandokey={tarea.id}. Cada tarea muestra su nombre, prioridad (con un color diferente), y un checkbox para marcarla como completada. - Filtros por estado: "Todas", "Pendientes", "Completadas". También un campo de búsqueda por nombre.
- Ordenar por prioridad (alta primero) o por nombre (A-Z).
- Un botón para eliminar tareas completadas.
- Muestra un contador: "X de Y tareas completadas".
Si no hay tareas que coincidan con los filtros, muestra un mensaje apropiado.
lightbulb Pistas
Usa un useState para las tareas, otro para el filtro de estado, otro para la búsqueda y otro para el orden. Los filtros son estado, pero la lista filtrada es estado derivado que se calcula directamente. Para los colores de prioridad, puedes usar un objeto: alta: 'red', media: 'orange', baja: 'green' . Para el checkbox, actualiza el estado de la tarea con map.