CicloUrbano ya responde a los clics, pero pinta siempre lo mismo: la tarjeta de una bicicleta en mantenimiento ofrece un botón «Reservar» que no debería estar ahí, y un catálogo sin resultados se queda en blanco sin explicar por qué. La interfaz tiene que decidir, y decidir es exactamente lo que hace el renderizado condicional. En React no existe una sintaxis especial para esto: como el JSX es JavaScript, se usan las herramientas del lenguaje —if, el operador ternario, &&, ??, objetos y switch—, cada una con su punto fuerte y su punto de ruptura. En esta lección verás las seis técnicas aplicadas al mismo caso, para poder compararlas de verdad, aprenderás cuándo conviene cada una y desactivarás una trampa clásica del operador && que puede colar un 0 suelto en mitad de tu interfaz.

Contenido

  1. El principio: interfaz = f(estado)
  2. Técnica 1: if/else antes del return
  3. Técnica 2: el operador ternario dentro del JSX
  4. Técnica 3: && para mostrar o no mostrar
  5. Técnica 4: ?? para valores ausentes
  6. Técnica 5: objeto de correspondencias
  7. Técnica 6: switch en una función auxiliar
  8. Tabla comparativa: cuándo usar cada una
  9. Retorno anticipado: no pintar nada
  10. La trampa del && con números y cadenas
  11. Componentes condicionales con nombre
  12. CicloUrbano: tarjetas que se adaptan al estado

  1. El principio: interfaz = f(estado)

En la primera lección del curso definimos React con una fórmula: la interfaz es una función de los datos. El renderizado condicional es la consecuencia directa de esa idea. No manipulas el DOM para ocultar un botón; describes qué debe aparecer para cada combinación de datos y dejas que React calcule la diferencia.

Compáralo con lo que harías sin React:

// JavaScript sin framework: das órdenes al DOM
const boton = document.querySelector('.boton-reservar');
if (bicicleta.estado === 'disponible') {
  boton.style.display = 'block';
} else {
  boton.style.display = 'none';
}
// React: describes el resultado, no los pasos
{bicicleta.estado === 'disponible' && <button>Reservar</button>}

La diferencia no es solo de longitud. En la versión imperativa el botón existe siempre en el DOM y tú te encargas de esconderlo; si mañana añades un tercer estado, hay que acordarse de tocar ese código. En la versión declarativa el botón no existe cuando no debe existir, y la condición está escrita justo donde se lee el marcado.

Y una idea que conviene fijar desde ya: en React, no pintar algo se expresa devolviendo un valor que no se pinta. Como viste en la lección 01-04, null, undefined, false y true no producen nada en pantalla. Todas las técnicas de esta lección se apoyan en esa propiedad.

  1. Técnica 1: if/else antes del return

La forma más legible cuando la condición afecta a una parte grande del resultado: se decide antes, se guarda en una variable y el JSX queda limpio.

// src/componentes/AvisoEstado.jsx

/**
 * Mensaje explicativo del estado de una bicicleta.
 * Props:
 *  - bicicleta (objeto, obligatorio)
 */
function AvisoEstado({ bicicleta }) {
  let mensaje;

  if (bicicleta.estado === 'disponible') {
    mensaje = <p className="aviso aviso--ok">Lista para salir.</p>;
  } else if (bicicleta.estado === 'alquilada') {
    mensaje = <p className="aviso aviso--espera">En uso; vuelve a consultar más tarde.</p>;
  } else {
    mensaje = <p className="aviso aviso--taller">En el taller: no se puede reservar.</p>;
  }

  return (
    <div className="aviso-estado">
      <h4>{bicicleta.modelo}</h4>
      {mensaje}
    </div>
  );
}

export default AvisoEstado;

Puntos importantes:

  • let mensaje guarda un elemento JSX. Recuerda de la lección 01-04 que un elemento JSX es un objeto JavaScript normal: se puede guardar en una variable, meter en un array o pasar como prop.
  • El if va fuera del return. Dentro del JSX solo caben expresiones, y if es una sentencia. Escribir {if (x) ...} dentro de las llaves es un error de sintaxis.
  • El JSX final se lee de un vistazo. Toda la lógica de decisión está arriba, agrupada, y el marcado abajo.

Esta técnica es la mejor cuando las ramas son largas o cuando hay que calcular otras cosas por el camino (un texto, una clase, un dato derivado).

  1. Técnica 2: el operador ternario dentro del JSX

El operador ternario condicion ? valorSiVerdadero : valorSiFalso sí es una expresión, así que puede ir dentro de las llaves. Es la herramienta para elegir entre dos alternativas concretas.

function AvisoDisponibilidad({ bicicleta }) {
  return (
    <p className="aviso">
      {bicicleta.estado === 'disponible'
        ? 'Lista para salir.'
        : 'No disponible ahora mismo.'}
    </p>
  );
}

Funciona igual con elementos completos, no solo con texto:

<div className="acciones">
  {bicicleta.estado === 'disponible' ? (
    <button type="button">Reservar</button>
  ) : (
    <span className="no-disponible">No reservable</span>
  )}
</div>

Fíjate en los paréntesis que envuelven cada rama cuando el JSX ocupa varias líneas: no son obligatorios sintácticamente, pero evitan errores del punto y coma automático y hacen el bloque mucho más legible.

También se usa constantemente para elegir clases o atributos, no solo contenido:

<article className={bicicleta.estado === 'disponible' ? 'tarjeta' : 'tarjeta tarjeta--atenuada'}>
<button type="button" disabled={bicicleta.estado !== 'disponible'}>Reservar</button>

Su límite es el anidamiento. Un ternario dentro de otro todavía se lee; tres ya no:

{/* Legible, en el límite */}
{estado === 'disponible' ? 'Libre' : estado === 'alquilada' ? 'En uso' : 'En taller'}

{/* Ilegible: esto es lo que hay que evitar */}
{estado === 'disponible'
  ? (rol === 'operario' ? <BotonTaller /> : <BotonReservar />)
  : (estado === 'alquilada' ? <Aviso texto="En uso" /> : <Aviso texto="Taller" />)}

Cuando llegues a ese punto, la respuesta no es «formatear mejor»: es cambiar de técnica (apartados 6, 7 y 11).

  1. Técnica 3: && para mostrar o no mostrar

Cuando la alternativa es «esto o nada», el ternario con : null sobra y el operador && es más directo.

{bicicleta.estado === 'disponible' && <button type="button">Reservar</button>}

Cómo funciona exactamente, porque no es magia de React sino JavaScript puro:

  1. && evalúa el operando de la izquierda.
  2. Si es falsy, devuelve ese operando y no evalúa la derecha.
  3. Si es truthy, devuelve el operando de la derecha.

Así que cuando la condición es false, la expresión completa vale false, y React no pinta los booleanos. Cuando es true, la expresión vale el elemento JSX, y React lo pinta.

Casos típicos:

{/* Un aviso que solo aparece cuando procede */}
{bicicleta.estado === 'mantenimiento' && (
  <p className="aviso-taller">Esta bicicleta está en revisión.</p>
)}

{/* Un contador que solo aparece si hay algo que contar */}
{bicicletasDisponibles > 0 && <span>{bicicletasDisponibles} disponibles</span>}

{/* Un bloque de administración solo para operarios */}
{usuario.rol === 'operario' && <PanelOperario usuario={usuario} />}

Es la técnica más usada de todas, y también la que más quebraderos de cabeza da por un motivo muy concreto que verás en el apartado 10. Guarda ya la regla: a la izquierda de && debe haber siempre un booleano.

  1. Técnica 4: ?? para valores ausentes

El operador de coalescencia nula (??) devuelve el operando de la derecha solo cuando el de la izquierda es null o undefined. Es el complemento perfecto para datos opcionales.

function FichaEstacion({ estacion, notas }) {
  return (
    <div className="ficha-estacion">
      <h3>{estacion.nombre}</h3>
      <p>Barrio: {estacion.barrio ?? 'Sin asignar'}</p>
      <p>Notas: {notas ?? 'Sin notas del operario.'}</p>
    </div>
  );
}

La diferencia con || es sutil pero decisiva, y en una aplicación con números y textos aparece constantemente:

Valor de la izquierda Con || Con ??
undefined usa el de la derecha usa el de la derecha
null usa el de la derecha usa el de la derecha
0 usa el de la derecha (mal) usa el 0
'' (cadena vacía) usa el de la derecha usa la cadena vacía
false usa el de la derecha usa false
'Centro' usa 'Centro' usa 'Centro'

El ejemplo que lo deja claro con datos de CicloUrbano:

const plazasLibres = 0;

<p>Plazas libres: {plazasLibres || 'desconocidas'}</p>   // -> "desconocidas"  ✘  ¡son 0!
<p>Plazas libres: {plazasLibres ?? 'desconocidas'}</p>   // -> "0"             ✔

Regla práctica: usa ?? para valores que pueden faltar y || solo cuando de verdad quieras tratar el 0 y la cadena vacía como «ausente».

  1. Técnica 5: objeto de correspondencias

Cuando hay tres o más casos enumerables, la solución más limpia no es una cadena de if ni un ternario anidado: es un objeto que asocia cada valor con lo que hay que pintar.

// src/componentes/EtiquetaEstado.jsx
import { clases } from '../utilidades/clases.js';
import estilos from './EtiquetaEstado.module.css';

// El mapa vive FUERA del componente: es una constante, no cambia entre renders
const ESTADOS = {
  disponible: { texto: 'Disponible', clave: 'disponible' },
  alquilada: { texto: 'Alquilada', clave: 'alquilada' },
  mantenimiento: { texto: 'En taller', clave: 'mantenimiento' }
};

const ESTADO_DESCONOCIDO = { texto: 'Estado desconocido', clave: 'desconocido' };

/**
 * Distintivo visual del estado de una bicicleta.
 * Props:
 *  - estado (cadena, opcional, por defecto 'disponible'):
 *    'disponible' | 'alquilada' | 'mantenimiento'
 */
function EtiquetaEstado({ estado = 'disponible' }) {
  const datos = ESTADOS[estado] ?? ESTADO_DESCONOCIDO;

  return (
    <span className={clases(estilos.etiqueta, estilos[datos.clave])}>{datos.texto}</span>
  );
}

export default EtiquetaEstado;

Qué gana esta versión respecto a la de 02-05:

  • El texto visible deja de ser el valor técnico. Antes pintaba literalmente mantenimiento; ahora pinta «En taller», que es lo que entiende una persona usuaria.
  • Añadir un cuarto estado es añadir una línea al objeto. No se toca el JSX ni la lógica.
  • ?? ESTADO_DESCONOCIDO cubre el caso imprevisto. Si algún día llega un estado no contemplado, la etiqueta lo dice en vez de quedarse vacía o romper.
  • El mapa está fuera del componente. Declararlo dentro lo recrearía en cada render sin necesidad, y además deja claro que es una constante del dominio, no un dato del componente.

El mismo patrón sirve para elegir componentes enteros, no solo textos:

import AvisoDisponible from './AvisoDisponible.jsx';
import AvisoAlquilada from './AvisoAlquilada.jsx';
import AvisoTaller from './AvisoTaller.jsx';

const AVISOS = {
  disponible: AvisoDisponible,
  alquilada: AvisoAlquilada,
  mantenimiento: AvisoTaller
};

function AvisoDeEstado({ bicicleta }) {
  // Ojo: la variable debe empezar por MAYÚSCULA para que JSX la trate como componente
  const Aviso = AVISOS[bicicleta.estado];

  if (!Aviso) {
    return null;
  }

  return <Aviso bicicleta={bicicleta} />;
}

Ahí aparece la regla de la mayúscula de la lección 01-03 en su forma más sutil: const Aviso = AVISOS[...] debe llamarse Aviso y no aviso, porque JSX interpreta las etiquetas en minúscula como elementos del DOM. <aviso /> intentaría crear una etiqueta HTML inexistente.

  1. Técnica 6: switch en una función auxiliar

El switch es una sentencia, así que no cabe en el JSX. Pero sí cabe en una función auxiliar que devuelve JSX y se invoca desde el marcado.

function MensajeReserva({ reserva }) {
  function renderizarMensaje() {
    switch (reserva.estado) {
      case 'activa':
        return <p className="msg msg--ok">Reserva activa. Recoge la bicicleta a tiempo.</p>;
      case 'finalizada':
        return <p className="msg">Reserva finalizada. Gracias por usar CicloUrbano.</p>;
      case 'cancelada':
        return <p className="msg msg--aviso">Reserva cancelada. No se ha realizado ningún cargo.</p>;
      default:
        return <p className="msg">Estado de reserva no reconocido.</p>;
    }
  }

  return (
    <section className="mensaje-reserva">
      <h4>Reserva {reserva.id}</h4>
      {renderizarMensaje()}
    </section>
  );
}

Dos observaciones:

  • La función se llama con paréntesis ({renderizarMensaje()}), porque queremos su resultado ahora, no una referencia. Es lo contrario de lo que hacías con los manejadores de eventos en 03-01: allí querías la función, aquí quieres el JSX que devuelve.
  • El default no es opcional en la práctica. Sin él, un valor inesperado hace que la función devuelva undefined y no se pinte nada, un fallo silencioso difícil de detectar.

¿Cuándo switch en vez de objeto de correspondencias? Cuando cada rama necesita lógica, no solo un valor: calcular algo, componer varios elementos, consultar otra prop.

  1. Tabla comparativa: cuándo usar cada una

Técnica Nº de casos Va dentro del JSX Legibilidad al anidar Cuándo usarla
if/else 2 o más No, antes del return Buena Ramas largas, o cuando hay que calcular más cosas por el camino
Ternario Exactamente 2 Mala a partir del segundo nivel Elegir entre dos textos, dos elementos, dos clases
&& 1 (mostrar o nada) Buena si la condición es simple Avisos, insignias, bloques opcionales
?? Valor o alternativa Buena Datos que pueden ser null o undefined
Objeto de correspondencias 3 o más Sí (consulta) Excelente: es plano Estados enumerables con texto, clase o componente asociado
switch auxiliar 3 o más Sí (llamada) Buena Casos enumerables donde cada rama tiene lógica propia
Retorno anticipado 1 (todo o nada) No Excelente Datos que faltan, permisos, estados de carga

La progresión natural al escribir código real: empiezas con &&, pasas a ternario cuando aparece la segunda alternativa y saltas a un objeto o a componentes con nombre cuando llegas al tercer caso. El síntoma de que te has pasado de técnica es tener que contar paréntesis para entender tu propio JSX.

  1. Retorno anticipado: no pintar nada

Un componente puede decidir que no debe pintarse en absoluto. La forma idiomática es devolver null cuanto antes.

// src/componentes/AvisoMantenimiento.jsx

/**
 * Aviso que solo aparece si la bicicleta está en el taller.
 * Props:
 *  - bicicleta (objeto, obligatorio)
 */
function AvisoMantenimiento({ bicicleta }) {
  // Retorno anticipado: si no hay nada que avisar, el componente no pinta nada
  if (bicicleta.estado !== 'mantenimiento') {
    return null;
  }

  return (
    <p className="aviso-taller" role="status">
      La bicicleta {bicicleta.modelo} está en revisión y no se puede reservar.
    </p>
  );
}

export default AvisoMantenimiento;

Detalles que conviene entender:

  • return null no es un error: es un valor válido de retorno que significa «no produzco marcado».
  • El componente se sigue montando y ejecutando. Devolver null no lo desmonta: React lo mantiene en el árbol, con su estado intacto, simplemente sin nodos en el DOM. Esta distinción importará cuando llegues a los efectos en el Módulo 5.
  • También sirve para protegerse de datos que faltan, y ese es su uso más frecuente:
function PanelReserva({ bicicleta }) {
  if (!bicicleta) {
    return <p>Selecciona una bicicleta para reservar.</p>;
  }

  // A partir de aquí sabemos que "bicicleta" existe: el resto del código va sin miedo
  return <div className="panel-reserva">…</div>;
}

Este patrón, llamado cláusula de guarda, evita que todo el cuerpo del componente quede envuelto en un if gigante y elimina de golpe los errores del tipo «Cannot read properties of undefined».

  1. La trampa del && con números y cadenas

En la lección 01-04 apareció el «cero fantasma» con plazasLibres. Ahora que && es tu herramienta principal, toca verlo en el escenario donde más muerde: contando elementos de una colección.

// MAL: cuando el catálogo está vacío, en pantalla aparece un "0" suelto
function Catalogo({ bicicletas }) {
  return (
    <section>
      <h2>Catálogo</h2>
      {bicicletas.length && <p>Se han encontrado {bicicletas.length} bicicletas.</p>}
    </section>
  );
}

Paso a paso, con bicicletas = []:

  1. bicicletas.length vale 0.
  2. 0 es falsy, así que && devuelve 0 sin evaluar la derecha.
  3. La expresión completa vale 0… y 0 es un número que React sí pinta.
  4. Resultado: en el hueco donde debería no haber nada aparece un 0 desconcertante.

Es exactamente lo que ocurre con false, salvo que false no se pinta y 0 sí. Las tres soluciones correctas:

{/* 1. Comparación explícita: la izquierda es un booleano de verdad. LA RECOMENDADA */}
{bicicletas.length > 0 && <p>Se han encontrado {bicicletas.length} bicicletas.</p>}

{/* 2. Ternario, que además deja escrito el caso contrario */}
{bicicletas.length > 0
  ? <p>Se han encontrado {bicicletas.length} bicicletas.</p>
  : <p>No hay bicicletas que mostrar.</p>}

{/* 3. Doble negación: correcta, pero comunica peor la intención */}
{!!bicicletas.length && <p>Se han encontrado {bicicletas.length} bicicletas.</p>}

El mismo problema, con matices, aparece con cadenas de texto:

const nota = '';

{nota && <p className="nota">{nota}</p>}

Aquí la cadena vacía es falsy, la expresión vale '' y React no pinta las cadenas vacías, así que visualmente no pasa nada malo. Pero el código sigue siendo frágil: si mañana esa variable puede valer '0' o un número, el fallo aparece. Escribe siempre la condición completa:

{nota !== '' && <p className="nota">{nota}</p>}
{typeof nota === 'string' && nota.length > 0 && <p className="nota">{nota}</p>}
Valor a la izquierda de && Qué devuelve la expresión Qué pinta React
false false Nada ✔
null / undefined ese valor Nada ✔
0 0 0
NaN NaN NaN
'' '' Nada (pero es frágil)
true el elemento JSX El elemento ✔

Regla definitiva: a la izquierda de &&, siempre una comparación. x > 0, x !== '', Boolean(x), x === 'disponible'. Nunca un valor a pelo.

  1. Componentes condicionales con nombre

Existe una técnica que no es un operador, sino una decisión de diseño: cuando el condicional se complica, extrae cada rama a un componente con un nombre que explique qué es.

Compara. Versión con ternarios anidados:

function AccionesBicicleta({ bicicleta, usuario }) {
  return (
    <div className="acciones">
      {bicicleta.estado === 'disponible'
        ? (usuario.rol === 'operario'
            ? <button type="button">Enviar a taller</button>
            : <button type="button">Reservar</button>)
        : (bicicleta.estado === 'alquilada'
            ? <span>En uso hasta las 12:00</span>
            : <span>En revisión</span>)}
    </div>
  );
}

Nadie quiere mantener eso. Versión con componentes con nombre:

// src/componentes/AccionesBicicleta.jsx

function AccionReservar({ bicicleta, alReservar }) {
  return (
    <button type="button" onClick={() => alReservar(bicicleta)}>
      Reservar por {bicicleta.precioHora.toFixed(2).replace('.', ',')} €/hora
    </button>
  );
}

function AccionTaller({ bicicleta, alEnviarATaller }) {
  return (
    <button type="button" onClick={() => alEnviarATaller(bicicleta)}>
      Enviar a taller
    </button>
  );
}

function AvisoNoDisponible({ bicicleta }) {
  const texto = bicicleta.estado === 'alquilada' ? 'En uso ahora mismo' : 'En revisión';
  return <span className="no-disponible">{texto}</span>;
}

/**
 * Acciones disponibles para una bicicleta según su estado y el rol del usuario.
 * Props:
 *  - bicicleta (objeto, obligatorio)
 *  - usuario (objeto, obligatorio) { id, nombre, email, rol }
 *  - alReservar, alEnviarATaller (funciones, opcionales)
 */
function AccionesBicicleta({ bicicleta, usuario, alReservar, alEnviarATaller }) {
  if (bicicleta.estado !== 'disponible') {
    return (
      <div className="acciones">
        <AvisoNoDisponible bicicleta={bicicleta} />
      </div>
    );
  }

  return (
    <div className="acciones">
      {usuario.rol === 'operario' ? (
        <AccionTaller bicicleta={bicicleta} alEnviarATaller={alEnviarATaller} />
      ) : (
        <AccionReservar bicicleta={bicicleta} alReservar={alReservar} />
      )}
    </div>
  );
}

export default AccionesBicicleta;

Lo que se ha ganado:

Antes Después
Un ternario de cuatro ramas anidadas Un retorno anticipado y un ternario plano
Los nombres de los casos no existen AccionReservar, AccionTaller, AvisoNoDisponible documentan el dominio
Cambiar un caso obliga a releer todo Cada caso se lee y se prueba por separado
Imposible reutilizar una rama Cualquier rama se puede usar en otra pantalla

Este es el mismo criterio de límites que aprendiste en la lección 02-01: si una rama del condicional tiene nombre propio en la conversación del equipo, merece ser un componente.

  1. CicloUrbano: tarjetas que se adaptan al estado

Aplicamos ahora lo aprendido a la aplicación. TarjetaBicicleta pasa a decidir tres cosas: si muestra el botón «Reservar», si muestra el aviso de taller y cómo describe el precio.

// src/componentes/TarjetaBicicleta.jsx
import { clases } from '../utilidades/clases.js';
import estilos from './TarjetaBicicleta.module.css';
import EtiquetaEstado from './EtiquetaEstado.jsx';

/**
 * Tarjeta de una bicicleta del catálogo de CicloUrbano.
 * Props:
 *  - bicicleta (objeto, obligatorio) { id, modelo, tipo, estado, estacionId, precioHora }
 *  - nombreEstacion (cadena, opcional, por defecto 'Estación desconocida')
 *  - alSeleccionar (función, opcional): recibe la bicicleta al pulsar la tarjeta
 *  - alReservar (función, opcional): recibe la bicicleta al pulsar «Reservar»
 */
function TarjetaBicicleta({
  bicicleta,
  nombreEstacion = 'Estación desconocida',
  alSeleccionar,
  alReservar
}) {
  // Valores derivados: se calculan en cada render, no son estado
  const disponible = bicicleta.estado === 'disponible';
  const enTaller = bicicleta.estado === 'mantenimiento';
  const precioFormateado = bicicleta.precioHora.toFixed(2).replace('.', ',');

  function manejarClicTarjeta() {
    if (alSeleccionar) {
      alSeleccionar(bicicleta);
    }
  }

  function manejarClicReservar(evento) {
    evento.stopPropagation();
    if (alReservar) {
      alReservar(bicicleta);
    }
  }

  return (
    <article
      className={clases(estilos.tarjeta, estilos[bicicleta.tipo], !disponible && estilos.atenuada)}
      onClick={manejarClicTarjeta}
    >
      <h3 className={estilos.titulo}>
        {bicicleta.modelo} <EtiquetaEstado estado={bicicleta.estado} />
      </h3>

      <p className={estilos.dato}>Tipo: {bicicleta.tipo}</p>
      <p className={estilos.dato}>Estación: {nombreEstacion ?? 'Estación desconocida'}</p>
      <p className={estilos.precio}>{precioFormateado} € / hora</p>

      {/* Aviso: se muestra o no se muestra. Caso de libro para && */}
      {enTaller && (
        <p className={estilos.avisoTaller}>En revisión: no admite reservas por ahora.</p>
      )}

      {/* Acción: dos alternativas concretas. Caso de libro para el ternario */}
      {disponible ? (
        <button type="button" className={estilos.accion} onClick={manejarClicReservar}>
          Reservar
        </button>
      ) : (
        <p className={estilos.sinAccion}>Vuelve a consultarla más tarde.</p>
      )}
    </article>
  );
}

export default TarjetaBicicleta;

Con los datos del dominio.js, el resultado por bicicleta es:

Bicicleta Estado Etiqueta Aviso de taller Acción
bici-001 Urbana Clásica disponible Disponible No Botón «Reservar»
bici-002 Eléctrica Pro alquilada Alquilada No «Vuelve a consultarla más tarde.»
bici-003 Carga Max mantenimiento En taller «Vuelve a consultarla más tarde.»
bici-004 Urbana Clásica disponible Disponible No Botón «Reservar»
bici-005 Eléctrica Pro disponible Disponible No Botón «Reservar»

El estado vacío del catálogo

Un catálogo que no encuentra nada y se queda en blanco es un fallo de diseño clásico: la persona usuaria no sabe si la aplicación está cargando, si se ha roto o si de verdad no hay resultados. La solución es un estado vacío explícito.

// src/componentes/ListaBicicletas.jsx
import TarjetaBicicleta from './TarjetaBicicleta.jsx';
import estilos from './ListaBicicletas.module.css';

/**
 * Sección del catálogo de CicloUrbano.
 * Props:
 *  - primera, segunda, tercera (objetos bicicleta, opcionales)
 *  - alSeleccionar, alReservar (funciones, opcionales)
 *
 * (En 03-03 estas tres props desaparecen y pasa a recibir el array completo.)
 */
function ListaBicicletas({ primera, segunda, tercera, alSeleccionar, alReservar }) {
  // Contamos cuántas props traen realmente una bicicleta
  const recibidas = [primera, segunda, tercera].filter(Boolean);
  const hayBicicletas = recibidas.length > 0;

  return (
    <section className={estilos.lista}>
      <h2>Bicicletas del catálogo</h2>

      {hayBicicletas ? (
        <>
          <p className={estilos.recuento}>
            {recibidas.length === 1
              ? 'Se ha encontrado 1 bicicleta.'
              : `Se han encontrado ${recibidas.length} bicicletas.`}
          </p>

          {primera && (
            <TarjetaBicicleta
              bicicleta={primera}
              nombreEstacion="Plaza Mayor"
              alSeleccionar={alSeleccionar}
              alReservar={alReservar}
            />
          )}
          {segunda && (
            <TarjetaBicicleta
              bicicleta={segunda}
              nombreEstacion="Plaza Mayor"
              alSeleccionar={alSeleccionar}
              alReservar={alReservar}
            />
          )}
          {tercera && (
            <TarjetaBicicleta
              bicicleta={tercera}
              nombreEstacion="Parque Norte"
              alSeleccionar={alSeleccionar}
              alReservar={alReservar}
            />
          )}
        </>
      ) : (
        <p className={estilos.vacio}>
          No hay bicicletas que coincidan con el filtro. Prueba con otro tipo.
        </p>
      )}
    </section>
  );
}

export default ListaBicicletas;

Sí, escribir tres veces la misma tarjeta con un && cada una es tedioso, y esa fealdad sigue siendo deliberada: es el último aviso antes del refactor. En la próxima lección, map elimina las tres props y las tres repeticiones de un plumazo.

El flujo de decisión completo de la tarjeta, resumido:

flowchart TD
    A["TarjetaBicicleta recibe bicicleta"] --> B{"¿estado === 'disponible'?"}
    B -- Sí --> C["Etiqueta 'Disponible'<br/>Botón «Reservar»"]
    B -- No --> D{"¿estado === 'mantenimiento'?"}
    D -- Sí --> E["Etiqueta 'En taller'<br/>Aviso de revisión<br/>Sin botón"]
    D -- No --> F["Etiqueta 'Alquilada'<br/>Mensaje 'vuelve más tarde'<br/>Sin botón"]

Errores Comunes y Consejos

  • Escribir if dentro del JSX. {if (x) ...} es un error de sintaxis: dentro de las llaves solo caben expresiones. Usa ternario, &&, o saca el if antes del return.
  • El 0 fantasma con &&. {lista.length && <p>…</p>} pinta un 0 cuando la lista está vacía. Escribe siempre una comparación: lista.length > 0 && ….
  • Usar || con números. {plazas || 'sin datos'} trata el 0 como ausencia. Usa ?? salvo que quieras justo lo contrario.
  • Ternarios anidados de tres o más niveles. Se vuelven imposibles de leer y de modificar. Pasa a un objeto de correspondencias o a componentes con nombre.
  • Olvidar el default en un switch. Un valor inesperado devuelve undefined y no se pinta nada, sin ningún error visible.
  • Nombrar en minúscula un componente sacado de un objeto. const aviso = AVISOS[estado] y luego <aviso /> crea una etiqueta HTML inexistente. Debe ser const Aviso = … y <Aviso />.
  • Creer que return null desmonta el componente. No lo hace: el componente sigue en el árbol con su estado. Para desmontarlo de verdad, el padre debe dejar de renderizarlo.
  • Ocultar con CSS lo que debería no renderizarse. style={{ display: 'none' }} deja el marcado en el DOM, con su coste y accesible para los lectores de pantalla. Si no debe estar, no lo pintes.
  • Definir el objeto de correspondencias dentro del componente. Se recrea en cada render sin motivo. Decláralo como constante fuera.
  • Consejo: piensa siempre en el caso vacío y en el caso de error. «Sin resultados», «cargando», «faltan datos». Las interfaces se rompen en los extremos, no en el caso feliz.
  • Consejo: extrae la condición a una variable con nombre. const disponible = bicicleta.estado === 'disponible' se lee mucho mejor dentro del JSX que la comparación completa repetida tres veces.
  • Consejo: si el marcado de dos ramas es casi idéntico, no dupliques el JSX. Condiciona solo lo que cambia (una clase, un texto, un atributo).

Ejercicios

Ejercicio 1

Este componente tiene cuatro problemas de renderizado condicional. Localízalos, explica el síntoma de cada uno y reescríbelo con las técnicas adecuadas.

function ResumenEstacion({ estacion, bicicletasEnEstacion, notaOperario }) {
  return (
    <div className="resumen-estacion">
      <h3>{estacion.nombre}</h3>
      <p>Barrio: {estacion.barrio || 'Sin asignar'}</p>
      {bicicletasEnEstacion.length && <p>{bicicletasEnEstacion.length} bicicletas aparcadas</p>}
      <p>Plazas libres: {estacion.plazas - bicicletasEnEstacion.length || 'ninguna'}</p>
      {notaOperario ? <p className="nota">{notaOperario}</p> : null}
    </div>
  );
}

Ejercicio 2

Crea el componente PanelReserva (src/componentes/PanelReserva.jsx) partiendo del que escribiste en 02-04. Debe:

  • Recibir bicicleta (que puede ser undefined) y horas con valor por defecto 1.
  • Si no hay bicicleta, mostrar «Selecciona una bicicleta del catálogo.» mediante un retorno anticipado.
  • Si la bicicleta no está disponible, mostrar un aviso con el motivo, distinto según sea alquilada o mantenimiento, usando un objeto de correspondencias.
  • Si está disponible, mostrar el modelo, las horas, el precio total calculado y un botón «Confirmar reserva».
  • Mostrar además, solo cuando el total supere los 10 €, un aviso de «Reserva de larga duración: se requiere confirmación por correo.»

Ejercicio 3

Para cada situación de CicloUrbano, indica qué técnica de las seis usarías y escribe la línea o el bloque correspondiente. Justifica la elección en una frase.

  1. Mostrar una insignia «Eléctrica» solo si bicicleta.tipo === 'electrica'.
  2. Pintar el icono de una estación según su barrio: Centro, Norte o Ensanche, con un icono distinto para cada uno.
  3. Mostrar «Cargando…» mientras bicicletas sea undefined y el catálogo cuando ya haya llegado.
  4. Mostrar el email del usuario o «Sin correo registrado» si vale null.
  5. Elegir entre un botón «Reservar» y uno «Cancelar reserva» según si el usuario ya tiene una reserva activa.

Soluciones

Solución 1.

Los cuatro problemas:

Línea Problema Síntoma
estacion.barrio || 'Sin asignar' Debería ser ?? Si el barrio fuese una cadena vacía legítima, se sustituiría; con ?? solo se sustituye null/undefined
bicicletasEnEstacion.length && … El 0 fantasma Con la estación vacía aparece un 0 suelto en pantalla
estacion.plazas - … || 'ninguna' 0 tratado como falsy Con la estación llena, 0 plazas libres se convierte en 'ninguna'; puede ser aceptable, pero es accidental, no intencionado, y además oculta el número real
notaOperario ? … : null Ternario con null innecesario Funciona, pero && es más directo. Y si notaOperario pudiera ser un número, el 0 volvería a colarse
// src/componentes/ResumenEstacion.jsx

/**
 * Resumen de ocupación de una estación de CicloUrbano.
 * Props:
 *  - estacion (objeto, obligatorio) { id, nombre, barrio, plazas }
 *  - bicicletasEnEstacion (array, opcional, por defecto [])
 *  - notaOperario (cadena, opcional)
 */
function ResumenEstacion({ estacion, bicicletasEnEstacion = [], notaOperario }) {
  const aparcadas = bicicletasEnEstacion.length;
  const libres = estacion.plazas - aparcadas;

  return (
    <div className="resumen-estacion">
      <h3>{estacion.nombre}</h3>
      <p>Barrio: {estacion.barrio ?? 'Sin asignar'}</p>

      {aparcadas > 0 ? (
        <p>{aparcadas === 1 ? '1 bicicleta aparcada' : `${aparcadas} bicicletas aparcadas`}</p>
      ) : (
        <p>No hay bicicletas aparcadas.</p>
      )}

      <p>Plazas libres: {libres}</p>
      {libres === 0 && <p className="aviso">Estación completa.</p>}

      {typeof notaOperario === 'string' && notaOperario.length > 0 && (
        <p className="nota">{notaOperario}</p>
      )}
    </div>
  );
}

export default ResumenEstacion;

Extraer aparcadas y libres a variables con nombre elimina de paso el cálculo repetido y hace las condiciones legibles.

Solución 2.

// src/componentes/PanelReserva.jsx
import estilos from './PanelReserva.module.css';

const MOTIVOS_NO_DISPONIBLE = {
  alquilada: 'Esta bicicleta está alquilada ahora mismo.',
  mantenimiento: 'Esta bicicleta está en el taller y no admite reservas.'
};

/**
 * Panel de confirmación de una reserva de CicloUrbano.
 * Props:
 *  - bicicleta (objeto, opcional): si falta, el panel invita a elegir una
 *  - horas (número, opcional, por defecto 1)
 *  - alConfirmar (función, opcional): recibe { bicicleta, horas, total }
 */
function PanelReserva({ bicicleta, horas = 1, alConfirmar }) {
  // Cláusula de guarda: sin bicicleta no hay nada que calcular
  if (!bicicleta) {
    return <p className={estilos.vacio}>Selecciona una bicicleta del catálogo.</p>;
  }

  const disponible = bicicleta.estado === 'disponible';

  if (!disponible) {
    const motivo = MOTIVOS_NO_DISPONIBLE[bicicleta.estado] ?? 'Esta bicicleta no está disponible.';
    return (
      <div className={estilos.panel}>
        <h3>{bicicleta.modelo}</h3>
        <p className={estilos.aviso}>{motivo}</p>
      </div>
    );
  }

  const total = bicicleta.precioHora * horas;
  const totalFormateado = total.toFixed(2).replace('.', ',');

  return (
    <div className={estilos.panel}>
      <h3>{bicicleta.modelo}</h3>
      <p>Duración: {horas === 1 ? '1 hora' : `${horas} horas`}</p>
      <p className={estilos.total}>Total: {totalFormateado} €</p>

      {total > 10 && (
        <p className={estilos.avisoLargo}>
          Reserva de larga duración: se requiere confirmación por correo.
        </p>
      )}

      <button
        type="button"
        onClick={() => alConfirmar && alConfirmar({ bicicleta, horas, total })}
      >
        Confirmar reserva
      </button>
    </div>
  );
}

export default PanelReserva;

Los dos retornos anticipados dejan el cuerpo principal libre de anidamiento: cuando el código llega a la última parte, ya sabemos con certeza que hay bicicleta y que está disponible. Y total, totalFormateado y disponible son valores derivados, no estado.

Solución 3.

Caso Técnica Código
1. Insignia «Eléctrica» && — es mostrar o nada {bicicleta.tipo === 'electrica' && <span className="insignia">Eléctrica</span>}
2. Icono por barrio Objeto de correspondencias — tres casos enumerables const ICONOS = { Centro: '🏛️', Norte: '🌳', Ensanche: '🚉' }; y luego {ICONOS[estacion.barrio] ?? '📍'}
3. «Cargando…» Retorno anticipado — todo o nada if (!bicicletas) { return <p>Cargando…</p>; } antes del return principal
4. Email o alternativa ?? — el dato puede ser null <p>{usuario.email ?? 'Sin correo registrado'}</p>
5. Reservar o cancelar Ternario — exactamente dos alternativas {tieneReservaActiva ? <BotonCancelar /> : <BotonReservar />}

En el caso 2, el ?? '📍' protege frente a un barrio nuevo que aún no tenga icono asignado: la interfaz muestra un marcador genérico en lugar de un hueco vacío.

Conclusión

El renderizado condicional en React no añade sintaxis nueva: aprovecha el JavaScript que ya conoces y lo combina con una propiedad clave del JSX, que null, undefined, false y true no producen marcado. Sobre esa base has visto seis técnicas y, sobre todo, cuándo usar cada una: el if/else antes del return para ramas largas; el ternario para elegir entre dos alternativas concretas, con el anidamiento como límite claro; el && para mostrar-o-nada, que es el caso más frecuente; el ?? para datos que pueden faltar, sin el problema que || tiene con el 0; el objeto de correspondencias para conjuntos enumerables de estados, plano y ampliable con una línea; y el switch en función auxiliar cuando cada rama necesita lógica propia. A eso se suman dos herramientas de estructura: el retorno anticipado con return null o con una cláusula de guarda, y la extracción de ramas a componentes con nombre cuando el condicional empieza a contar paréntesis.

Has cerrado además la trampa del && que quedó abierta en la lección de JSX, y ahora la conoces en su forma más peligrosa: {lista.length && …} pinta un 0 en pantalla cuando la lista está vacía. La regla es innegociable: a la izquierda de &&, siempre una comparación.

CicloUrbano lo nota: EtiquetaEstado traduce el estado técnico a un texto legible mediante un mapa ampliable, TarjetaBicicleta oculta el botón «Reservar» de las bicicletas que no lo admiten y avisa de las que están en el taller, y el catálogo ya tiene un estado vacío que explica qué ha pasado en lugar de dejar un hueco en blanco.

Pero la lista sigue escribiéndose tarjeta a tarjeta, con tres props numeradas y tres bloques && casi idénticos. Es la última pieza fea que queda del Módulo 2, y también la deuda que arrastramos desde la lección de reconciliación, donde se prometió explicar para qué sirven realmente las key. Las dos se saldan a la vez en Listas y Claves, donde una sola línea con map sustituirá a todo ese andamiaje.

Curso de React

Módulo 1: Introducción a React

Módulo 2: Componentes de React

Módulo 3: Trabajando con Eventos

Módulo 4: Conceptos Avanzados de Componentes

Módulo 5: Hooks de React

Módulo 6: Enrutamiento en React

Módulo 7: Gestión del Estado

Módulo 8: Optimización del Rendimiento

Módulo 9: Pruebas en React

Módulo 10: Temas Avanzados

Módulo 11: Proyecto: Construyendo una Aplicación Completa

© Copyright 2026. Todos los derechos reservados