Saltar al contenido
schedule 10 min React

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:

src/components/ListaCafes.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

src/components/MenuCafe.jsx
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 &&

Texto

} renderiza 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:

src/components/ListaConEstados.jsx
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:

src/components/MenuFiltrable.jsx
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

  • map transforma 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.
  • filter y sort combinados con map permiten 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.

code

Gestor de tareas espaciales

Intermedio schedule 25 min

Crea un gestor de tareas para misiones espaciales con las siguientes funcionalidades:

  • Un formulario para añadir tareas con nombre, prioridad (alta, media, baja) y completada (por defecto false). Genera un id único con Date.now().
  • Muestra la lista de tareas con map, usando key={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.

Newsletter

Recibe nuevos cursos, actualizaciones, artículos del blog y promociones en tu correo.