Con el catálogo de CicloUrbano ya filtrando de verdad, App empieza a crecer: secciones que envolver, avisos que mostrar, paneles que se repiten con pequeñas variaciones. Quien viene de Java, C# o PHP tiene aquí un reflejo automático: crear una clase base PanelBase y hacer que PanelCatalogo y PanelReservas hereden de ella. En React ese reflejo es el equivocado. La documentación oficial es explícita: no se ha encontrado ningún caso de uso en el que la herencia de componentes sea preferible a la composición. En esta lección aprenderás por qué, y qué se usa en su lugar: children, huecos con nombre, especialización por configuración, render props y componentes de orden superior, con una regla final que ordena todas las decisiones de reutilización que tomarás en React.

Contenido

  1. Por qué React no usa herencia de componentes
  2. Composición con children: contenedores genéricos
  3. Huecos con nombre: varias props que reciben JSX
  4. Especialización por configuración: de Aviso a AvisoMantenimiento
  5. children como función: las render props
  6. Componentes de orden superior (HOC): cómo se leen
  7. Tabla comparativa de las cinco técnicas
  8. La regla práctica: componer interfaz, extraer lógica
  9. Ejemplo central: la pantalla de catálogo recompuesta

  1. Por qué React no usa herencia de componentes

La herencia clásica resuelve la reutilización diciendo «B es un A y además hace esto otro». Funciona bien con jerarquías de datos estables, y muy mal con interfaces, por tres razones concretas.

Primera: las interfaces no forman jerarquías limpias. ¿Un panel de reservas es un panel? ¿Y si además necesita el comportamiento de un modal y el de una lista filtrable? Con herencia simple tendrías que elegir uno; con jerarquías profundas acabas con PanelBaseConCabeceraYAcciones, un nombre que confiesa el problema.

Segunda: la herencia acopla por dentro. La subclase depende de detalles internos de la clase padre. Cambiar el padre rompe hijos que ni siquiera has abierto. En una base de código con doscientos componentes eso es inmanejable.

Tercera, y decisiva: React ya tiene una relación de contención mejor. Un componente no necesita ser otro para reutilizarlo: basta con usarlo en su JSX. Eso es composición, y produce una relación «tiene un» en lugar de «es un».

// HERENCIA (no lo hagas en React)
class PanelReservas extends Panel {   // "PanelReservas ES UN Panel"
  render() { /* … y ahora, ¿cómo reutilizo lo que pintaba Panel? */ }
}

// COMPOSICIÓN (lo idiomático)
function PanelReservas({ reservas }) {          // "PanelReservas TIENE UN Panel"
  return (
    <Panel titulo="Mis reservas">
      <ListaReservas reservas={reservas} />
    </Panel>
  );
}

Y hay un argumento técnico que zanja la discusión: los componentes de función no se pueden extender. No hay clase, no hay extends, no hay super. Desde que los hooks se convirtieron en la forma estándar de escribir React, la herencia de componentes dejó de ser siquiera una opción disponible. Las clases sobreviven como código heredado (04-03) y para los límites de error (04-05), pero incluso ahí se extiende Component, la clase de React, jamás un componente tuyo.

  1. Composición con children: contenedores genéricos

children es la prop especial que ya conoces desde 02-03: contiene todo lo que se escribe entre la etiqueta de apertura y la de cierre de un componente. Es la herramienta de composición número uno, y sirve para escribir contenedores genéricos: componentes que aportan estructura y estilo sin saber qué van a contener.

El Panel de CicloUrbano es exactamente eso:

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

/**
 * Contenedor con título y contenido libre.
 * Props:
 *  - titulo   (cadena, obligatorio)
 *  - children (contenido, obligatorio)
 *  - pie      (elemento JSX, opcional)
 */
function Panel({ titulo, children, pie }) {
  return (
    <section className={estilos.panel}>
      <h2 className={estilos.titulo}>{titulo}</h2>
      <div className={estilos.cuerpo}>{children}</div>
      {pie && <div className={estilos.pie}>{pie}</div>}
    </section>
  );
}

export default Panel;

Panel no sabe absolutamente nada de bicicletas, y por eso sirve para todo:

<Panel titulo="Catálogo" pie={<small>Precios con IVA incluido.</small>}>
  <ListaBicicletas bicicletas={bicicletasVisibles} estaciones={estaciones} />
</Panel>

<Panel titulo="Mis reservas">
  <ListaReservas reservas={reservas} />
</Panel>

Este es el punto que hay que interiorizar: el hueco children es lo que la herencia intentaba conseguir con métodos abstractos, pero sin acoplar nada. El contenedor define el marco; quien lo usa decide el contenido.

Dos contenedores más de la misma familia:

// src/componentes/Diseño.jsx  (fichero: Diseno.jsx, sin ñ)
import estilos from './Diseno.module.css';

/**
 * Estructura general de una pantalla de CicloUrbano.
 * Props:
 *  - children (contenido principal)
 */
function Diseno({ children }) {
  return (
    <div className={estilos.diseno}>
      <Cabecera />
      <main className={estilos.principal}>{children}</main>
      <PieDePagina />
    </div>
  );
}

export default Diseno;
// src/componentes/Modal.jsx
import estilos from './Modal.module.css';

/**
 * Ventana modal genérica.
 * Props:
 *  - titulo   (cadena, obligatorio)
 *  - children (contenido)
 *  - alCerrar (función, obligatorio)
 */
function Modal({ titulo, children, alCerrar }) {
  return (
    <div className={estilos.fondo} onClick={alCerrar}>
      <div
        className={estilos.ventana}
        role="dialog"
        aria-modal="true"
        aria-label={titulo}
        onClick={(evento) => evento.stopPropagation()}
      >
        <h2>{titulo}</h2>
        {children}
        <button type="button" onClick={alCerrar}>Cerrar</button>
      </div>
    </div>
  );
}

export default Modal;

El stopPropagation() del segundo onClick es el de 03-01: sin él, un clic dentro de la ventana burbujearía hasta el fondo y cerraría el modal. Fíjate en que Modal tampoco sabe qué contiene: puede envolver un PanelReserva, un FormularioReserva o un texto de confirmación, sin cambiar una línea.

Nota sobre el nombre del fichero: el componente se llama Diseño en el código si quieres, pero el fichero debe ser Diseno.jsx, sin ñ, para que la ruta sea segura en cualquier sistema. En este curso usaremos Diseno en ambos sitios por coherencia.

  1. Huecos con nombre: varias props que reciben JSX

Un único children se queda corto en cuanto el contenedor tiene varias zonas. La solución no es inventar convenciones sobre el orden de los hijos, sino aceptar JSX en props normales. A cada prop de este tipo se le llama hueco con nombre (named slot).

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

/**
 * Panel con cuatro huecos independientes.
 * Props:
 *  - titulo   (cadena, obligatorio)
 *  - cabecera (JSX, opcional): contenido extra bajo el título
 *  - acciones (JSX, opcional): botones de la esquina superior derecha
 *  - children (JSX): el cuerpo
 *  - pie      (JSX, opcional)
 */
function PanelAvanzado({ titulo, cabecera, acciones, children, pie }) {
  return (
    <section className={estilos.panel}>
      <header className={estilos.encabezado}>
        <div>
          <h2 className={estilos.titulo}>{titulo}</h2>
          {cabecera}
        </div>
        {acciones && <div className={estilos.acciones}>{acciones}</div>}
      </header>

      <div className={estilos.cuerpo}>{children}</div>

      {pie && <footer className={estilos.pie}>{pie}</footer>}
    </section>
  );
}

export default PanelAvanzado;

Y su uso en el catálogo de CicloUrbano:

<PanelAvanzado
  titulo="Catálogo de bicicletas"
  cabecera={<p>Mostrando {bicicletasVisibles.length} de {bicicletas.length} bicicletas.</p>}
  acciones={
    <SelectorTipo tipoElegido={tipoElegido} alCambiarTipo={manejarCambioTipo} />
  }
  pie={<small>Precios con IVA incluido · Tarifa mínima: 1 hora</small>}
>
  <ListaBicicletas
    bicicletas={bicicletasVisibles}
    estaciones={estaciones}
    alSeleccionar={manejarSeleccionBicicleta}
    alReservar={manejarReserva}
  />
</PanelAvanzado>

No hay nada mágico: una prop puede contener JSX igual que contiene un número o una cadena, porque JSX es solo un valor de JavaScript. El propio Panel original ya lo hacía con pie.

Cuándo elegir cada opción:

Situación Técnica
El contenedor tiene una sola zona de contenido children
El contenedor tiene dos o más zonas con significados distintos Huecos con nombre
Alguna zona es opcional y debe poder omitirse limpiamente Huecos con nombre + {zona && …}
El orden o el número de hijos es libre children
Necesitas que la zona sea reconocible al leer el código de quien lo usa Huecos con nombre (acciones={…} se autoexplica)

Una ventaja poco evidente de los huecos con nombre: el JSX que pasas se crea en el componente padre, así que puede leer el estado del padre sin problemas. En el ejemplo, SelectorTipo recibe tipoElegido de App aunque se pinte dentro de PanelAvanzado, que no sabe nada de filtros. La posición en el árbol y la propiedad de los datos son cosas distintas.

  1. Especialización por configuración: de Aviso a AvisoMantenimiento

Aquí está el sustituto directo de la herencia. Cuando quieras un «caso concreto» de un componente genérico, no lo extiendas: escribe otro componente que lo use con props fijas.

Primero, el componente genérico:

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

const SIMBOLOS = {
  info: 'ℹ',
  exito: '✓',
  advertencia: '!',
  error: '×'
};

/**
 * Aviso genérico de CicloUrbano.
 * Props:
 *  - tono     (cadena: 'info' | 'exito' | 'advertencia' | 'error'; por defecto 'info')
 *  - titulo   (cadena, opcional)
 *  - children (contenido del aviso)
 *  - acciones (JSX, opcional): botones al pie del aviso
 */
function Aviso({ tono = 'info', titulo, children, acciones }) {
  const urgente = tono === 'error' || tono === 'advertencia';

  return (
    <div
      className={clases(estilos.aviso, estilos[tono])}
      role={urgente ? 'alert' : 'status'}
    >
      <span className={estilos.simbolo} aria-hidden="true">{SIMBOLOS[tono]}</span>
      <div className={estilos.contenido}>
        {titulo && <p className={estilos.titulo}>{titulo}</p>}
        <div>{children}</div>
        {acciones && <div className={estilos.acciones}>{acciones}</div>}
      </div>
    </div>
  );
}

export default Aviso;

Detalles que arrastran lecciones anteriores: el aria-hidden del símbolo es el mismo patrón que EtiquetaEstado (03-06), porque un lector de pantalla no debe leer «signo de admiración»; y el role cambia según la urgencia, alert para lo que interrumpe y status para lo que informa.

Ahora, las especializaciones. Ni una sola palabra clave de herencia:

// src/componentes/AvisoMantenimiento.jsx
import Aviso from './Aviso.jsx';

/**
 * Aviso específico para bicicletas en taller.
 * Props:
 *  - bicicleta (objeto, obligatorio)
 *  - alAvisarOperario (función, opcional)
 */
function AvisoMantenimiento({ bicicleta, alAvisarOperario }) {
  return (
    <Aviso
      tono="advertencia"
      titulo="Bicicleta en mantenimiento"
      acciones={
        alAvisarOperario && (
          <button type="button" onClick={() => alAvisarOperario(bicicleta)}>
            Avisar a un operario
          </button>
        )
      }
    >
      La bicicleta <strong>{bicicleta.modelo}</strong> ({bicicleta.id}) está en el taller
      y no admite reservas. Consulta el catálogo para ver alternativas disponibles.
    </Aviso>
  );
}

export default AvisoMantenimiento;
// src/componentes/AvisoReservaCreada.jsx
import Aviso from './Aviso.jsx';

/**
 * Confirmación de una reserva recién creada.
 * Props:
 *  - reserva  (objeto Reserva, obligatorio)
 *  - alCerrar (función, opcional)
 */
function AvisoReservaCreada({ reserva, alCerrar }) {
  const fecha = new Date(reserva.fechaInicio).toLocaleString('es-ES');

  return (
    <Aviso
      tono="exito"
      titulo="Reserva confirmada"
      acciones={
        alCerrar && (
          <button type="button" onClick={alCerrar}>Entendido</button>
        )
      }
    >
      Tu reserva <strong>{reserva.id}</strong> queda registrada para el {fecha},
      con una duración de {reserva.horas === 1 ? '1 hora' : `${reserva.horas} horas`}.
    </Aviso>
  );
}

export default AvisoReservaCreada;

Compara los dos modelos mentales:

Con herencia Con especialización por configuración
class AvisoMantenimiento extends Aviso function AvisoMantenimiento() { return <Aviso …/> }
Hay que conocer los métodos internos del padre para sobrescribirlos Solo hay que conocer las props públicas, su contrato documentado
Cambiar Aviso puede romper la subclase de forma invisible Cambiar Aviso rompe, como mucho, el contrato de props, y el error es evidente
La subclase no puede especializar dos padres AvisoMantenimiento puede usar Aviso y Modal a la vez
Difícil de probar por separado Cada componente se prueba con props (Módulo 9)

Y el beneficio práctico: cuando el diseñador cambie el aspecto de los avisos, tocas Aviso.jsx y los tres se actualizan. Cuando el texto de mantenimiento cambie, tocas AvisoMantenimiento.jsx y nada más se entera.

  1. children como función: las render props

Hay un caso que children normal no cubre: cuando el contenedor tiene datos que el contenido necesita. Un contenedor que filtra una lista sabe qué elementos quedan, pero no cómo hay que pintarlos.

La solución clásica: pasar una función como children, a la que el contenedor llama con los datos. Se llama render prop (o children as a function).

// src/componentes/ListaFiltrable.jsx
import { useState } from 'react';
import estilos from './ListaFiltrable.module.css';

/**
 * Contenedor que filtra una colección por texto y delega el pintado.
 * Props:
 *  - elementos     (array, obligatorio)
 *  - campoBusqueda (función, obligatorio): de un elemento devuelve el texto buscable
 *  - etiqueta      (cadena, opcional): texto del campo de búsqueda
 *  - children      (FUNCIÓN, obligatorio): recibe los elementos filtrados y devuelve JSX
 */
function ListaFiltrable({ elementos, campoBusqueda, etiqueta = 'Buscar', children }) {
  const [texto, setTexto] = useState('');   // estado LOCAL: no se eleva (04-01)

  const termino = texto.trim().toLowerCase();
  const filtrados =
    termino === ''
      ? elementos
      : elementos.filter((elemento) =>
          campoBusqueda(elemento).toLowerCase().includes(termino)
        );

  return (
    <div className={estilos.contenedor}>
      <label className={estilos.etiqueta}>
        {etiqueta}
        <input
          type="search"
          value={texto}
          onChange={(evento) => setTexto(evento.target.value)}
        />
      </label>

      <p aria-live="polite" className={estilos.recuento}>
        {filtrados.length === 1
          ? '1 resultado'
          : `${filtrados.length} resultados`}
      </p>

      {children(filtrados)}   {/* ← aquí está la clave: children es una función */}
    </div>
  );
}

export default ListaFiltrable;

Uso con bicicletas:

<ListaFiltrable
  elementos={bicicletas}
  campoBusqueda={(bicicleta) => bicicleta.modelo}
  etiqueta="Buscar por modelo"
>
  {(visibles) => (
    <ListaBicicletas bicicletas={visibles} estaciones={estaciones} />
  )}
</ListaFiltrable>

Y el mismo contenedor, sin tocarlo, con estaciones:

<ListaFiltrable
  elementos={estaciones}
  campoBusqueda={(estacion) => `${estacion.nombre} ${estacion.barrio}`}
  etiqueta="Buscar por estación o barrio"
>
  {(visibles) => (
    <ul>
      {visibles.map((estacion) => (
        <li key={estacion.id}>
          <TarjetaEstacion estacion={estacion} />
        </li>
      ))}
    </ul>
  )}
</ListaFiltrable>

Qué está pasando, línea a línea:

  • children deja de ser JSX y pasa a ser una función. JSX admite cualquier valor entre llaves, incluida una función; lo único que cambia es que el contenedor la llama en vez de pintarla.
  • children(filtrados) es una llamada normal de JavaScript. El contenedor decide qué datos entrega; quien lo usa decide cómo se pintan.
  • La lógica de filtrado vive en un solo sitio y sirve para bicicletas, estaciones o reservas.
  • El estado texto no se eleva, en aplicación directa de 04-01: nadie fuera del contenedor lo necesita.

Nota importante. Hoy este problema casi siempre se resuelve mejor con un hook personalizado: const { texto, setTexto, filtrados } = useFiltroTexto(elementos, campoBusqueda). La lógica se reutiliza igual, pero sin añadir un nivel al árbol de componentes y sin anidar funciones dentro del JSX. Lo verás en Hooks Personalizados. Las render props siguen siendo útiles cuando además del dato hay marcado que compartir —el campo de búsqueda y el recuento, en este ejemplo—, y aparecen en bibliotecas muy usadas, así que hay que saber leerlas.

  1. Componentes de orden superior (HOC): cómo se leen

Un componente de orden superior (higher-order component) es una función que recibe un componente y devuelve otro componente con capacidades añadidas. No es una función de React: es un patrón, igual que los decoradores de otros lenguajes.

Se reconocen al instante por su nombre (con…, o with… en inglés) y por la doble llamada al exportar.

// src/hoc/conAutenticacion.jsx  — patrón heredado, para saber leerlo

/**
 * Envuelve un componente y le exige un usuario autenticado.
 * conAutenticacion(Componente) -> Componente protegido
 */
function conAutenticacion(Componente) {
  function ComponenteProtegido(props) {
    const usuario = leerUsuarioDeSesion();   // fuente ficticia

    if (!usuario) {
      return <Aviso tono="advertencia" titulo="Acceso restringido">
        Inicia sesión para ver esta sección de CicloUrbano.
      </Aviso>;
    }

    return <Componente {...props} usuario={usuario} />;
  }

  // Buena práctica: nombre legible en React DevTools
  ComponenteProtegido.displayName = `conAutenticacion(${Componente.name})`;

  return ComponenteProtegido;
}

export default conAutenticacion;
// Uso
import conAutenticacion from '../hoc/conAutenticacion.jsx';

function PanelOperario({ usuario, bicicletas }) {
  return <p>Hola, {usuario.nombre}. Hay {bicicletas.length} bicicletas en flota.</p>;
}

export default conAutenticacion(PanelOperario);

Claves para leer código con HOC:

  • El componente exportado no es el que has escrito, sino el envoltorio. Por eso en las DevTools ves conAutenticacion(PanelOperario) y no PanelOperario a secas.
  • {...props} es el spread de 02-03, y es obligatorio: sin él, el envoltorio se traga las props y el componente interior se queda sin datos.
  • El HOC inyecta props nuevas (usuario), que aparecen «de la nada» en la firma del componente interior. Esa es justamente la crítica principal: leyendo PanelOperario no hay forma de saber de dónde sale usuario.

El problema se agrava al acumularlos:

export default conAutenticacion(conTema(conRegistro(conDatosDeFlota(PanelOperario))));

Ese apilamiento produce el infierno de envoltorios (wrapper hell) que se mencionó en 02-02: un árbol con cinco capas que no pintan nada, props que aparecen sin origen visible, colisiones de nombres entre HOC y depuración incómoda. Los hooks lo sustituyen por algo mucho más plano:

// Lo mismo, con hooks (Módulo 5)
function PanelOperario({ bicicletas }) {
  const usuario = useUsuario();     // se ve de dónde sale
  const tema = useTema();
  // …
}

Sin envoltorios, sin props inyectadas, con el origen de cada dato a la vista en la propia línea. Por eso los HOC han quedado desplazados: no los escribas en código nuevo, pero espera encontrarlos en proyectos con años a sus espaldas (connect() de Redux, que verás en 07-05, es el HOC más famoso de la historia de React).

  1. Tabla comparativa de las cinco técnicas

Técnica Reutiliza Legibilidad Cuándo usarla Estado en React 19
Herencia de componentes Nada útil Mala: acopla por dentro Nunca Descartada; imposible con funciones
Composición con children Estructura y estilo Excelente: se lee como HTML Contenedores con una zona de contenido Recomendada, es la opción por defecto
Huecos con nombre Estructura con varias zonas Muy buena: cada hueco se autoexplica Cabecera, acciones, pie, barra lateral Recomendada
Render props Lógica y marcado a la vez Media: anida funciones en el JSX Cuando el contenedor aporta datos y marcado Válida, pero minoritaria
Componentes de orden superior Lógica y comportamiento Baja: props de origen invisible Solo para mantener código existente Desplazada por los hooks
Hooks personalizados Lógica con estado, sin marcado Excelente: es una llamada a función Lógica reutilizable sin interfaz propia Recomendada (05-06)

  1. La regla práctica: componer interfaz, extraer lógica

Todo lo anterior se resume en una frase que conviene memorizar:

Componer para la interfaz; extraer funciones o hooks para la lógica.

Aplicada como árbol de decisión:

flowchart TD
    Q1{"¿Qué quiero reutilizar?"}
    Q1 -- "Marcado y estructura" --> Q2{"¿Cuántas zonas de contenido?"}
    Q2 -- "Una" --> C1["children"]
    Q2 -- "Varias" --> C2["Huecos con nombre"]
    Q1 -- "Una variante concreta<br/>de un componente" --> C3["Especialización<br/>por configuración"]
    Q1 -- "Lógica SIN estado" --> C4["Una función normal<br/>en src/utilidades/"]
    Q1 -- "Lógica CON estado" --> C5["Hook personalizado<br/>(05-06)"]
    Q1 -- "Datos para todo<br/>un subárbol" --> C6["Contexto<br/>(05-04)"]

Dos ejemplos ya presentes en CicloUrbano confirman los extremos del diagrama: clases() en src/utilidades/clases.js y validarReserva() en src/utilidades/validarReserva.js son lógica sin estado, y por eso son funciones normales, no componentes ni HOC. No hacía falta ningún patrón para reutilizarlas: bastó con exportarlas.

  1. Ejemplo central: la pantalla de catálogo recompuesta

Juntemos las piezas. App quedó así al final de 04-01, con toda la estructura escrita a pelo:

// src/App.jsx  — ANTES
return (
  <>
    <Cabecera />
    <main>
      <ResumenFlota flota={bicicletas} />
      <SelectorTipo tipoElegido={tipoElegido} alCambiarTipo={manejarCambioTipo} />
      <ListaBicicletas bicicletas={bicicletasVisibles} estaciones={estaciones} … />
      <PanelReserva bicicleta={bicicletaSeleccionada} … />
    </main>
    <PieDePagina />
  </>
);

Y así queda componiendo Diseno + PanelAvanzado + AvisoMantenimiento:

// src/App.jsx  — DESPUÉS (fragmento del return)
return (
  <Diseno>
    <ResumenFlota flota={bicicletas} />

    <PanelAvanzado
      titulo="Catálogo de bicicletas"
      cabecera={
        <p aria-live="polite">
          Mostrando {bicicletasVisibles.length} de {bicicletas.length} bicicletas.
        </p>
      }
      acciones={<SelectorTipo tipoElegido={tipoElegido} alCambiarTipo={manejarCambioTipo} />}
      pie={<small>Precios con IVA incluido · Tarifa mínima: 1 hora</small>}
    >
      <ListaBicicletas
        bicicletas={bicicletasVisibles}
        estaciones={estaciones}
        alSeleccionar={manejarSeleccionBicicleta}
        alReservar={manejarReserva}
      />
    </PanelAvanzado>

    <PanelAvanzado titulo="Reserva">
      {bicicletaSeleccionada && bicicletaSeleccionada.estado === 'mantenimiento' ? (
        <AvisoMantenimiento
          bicicleta={bicicletaSeleccionada}
          alAvisarOperario={manejarAvisoOperario}
        />
      ) : (
        <PanelReserva
          bicicleta={bicicletaSeleccionada}
          horas={horasReserva}
          alCambiarHoras={manejarCambioHoras}
          alConfirmar={manejarConfirmacion}
        />
      )}
    </PanelAvanzado>
  </Diseno>
);

El árbol resultante:

flowchart TD
    APP["App<br/>estado: tipoElegido, bicicletaSeleccionada, horasReserva"]
    APP --> DIS["Diseno<br/>(children)"]
    DIS --> CAB["Cabecera"]
    DIS --> MAIN["main"]
    DIS --> PIE["PieDePagina"]
    MAIN --> RES["ResumenFlota"]
    MAIN --> PA1["PanelAvanzado «Catálogo»"]
    MAIN --> PA2["PanelAvanzado «Reserva»"]
    PA1 -- "acciones" --> SEL["SelectorTipo"]
    PA1 -- "children" --> LIS["ListaBicicletas"]
    PA2 -- "children" --> PR["PanelReserva o AvisoMantenimiento"]
    AV["AvisoMantenimiento"] --> AVG["Aviso (genérico)"]

Qué se ha ganado, en términos concretos:

  • App vuelve a leerse como un índice de la pantalla, el objetivo que se fijó en 02-01. La estructura visual (cabecera, main, pie) se ha ido a Diseno y no se repetirá en cada pantalla nueva que añadas.
  • Ni una sola herencia. Cinco componentes reutilizados, cero extends.
  • Aviso es el único que conoce los colores, los símbolos y los roles ARIA de los mensajes; AvisoMantenimiento y AvisoReservaCreada solo aportan su texto y su tono.
  • Los huecos con nombre acercan el filtro a lo que filtra: SelectorTipo se pinta en la cabecera del panel del catálogo, aunque su estado siga viviendo en App. Posición y propiedad del dato son cosas independientes.

Errores Comunes y Consejos

  • Intentar extends MiComponente. No compila con componentes de función y no aporta nada con clases. Si sientes la necesidad, casi siempre buscas un contenedor con children o una especialización por configuración.
  • Olvidar el spread en un HOC. <Componente usuario={usuario} /> sin {...props} deja al componente interior sin las props que le llegaban desde fuera, y el fallo se manifiesta como datos vacíos sin ningún error.
  • Convertir children en función sin avisar. Si ListaFiltrable hace children(filtrados) y alguien le pasa JSX normal, el error es children is not a function. Documéntalo en el bloque de props y, si el componente admite ambas formas, comprueba typeof children === 'function'.
  • Crear un contenedor para cada variación mínima. Antes de escribir PanelPequeno, PanelGrande y PanelRojo, prueba con una prop variante en un único Panel. La especialización se justifica cuando hay contenido o comportamiento propios, no solo una clase CSS distinta.
  • Anidar demasiados contenedores. <Diseno><Panel><Tarjeta><Panel>… con seis niveles complica la lectura tanto como el infierno de envoltorios que criticábamos. Si el árbol se vuelve ilegible, extrae una pantalla completa a su propio componente.
  • Consejo: componente genérico primero, especializaciones después. Escribe Aviso resolviendo el caso concreto que tienes delante y extrae AvisoMantenimiento cuando aparezca el segundo uso. Generalizar con un solo caso lleva a abstracciones equivocadas.
  • Consejo: las props que reciben JSX se nombran por su zona, no por su contenido. acciones, cabecera y pie describen dónde va el contenido; botonReservar describiría qué es, y ataría el contenedor a un caso concreto.

Ejercicios

Ejercicio 1. Crea TarjetaResumen, un contenedor con huecos con nombre y una única zona de contenido libre. Props: titulo (cadena), icono (JSX, opcional, se pinta a la izquierda del título con aria-hidden), children (cuerpo) y acciones (JSX, opcional, al pie). Después úsalo dos veces en la pantalla de CicloUrbano: una para el resumen de la flota y otra para la estación est-01.

Ejercicio 2. Partiendo del Aviso genérico del apartado 4, crea AvisoSinResultados, que se muestre cuando el filtro no devuelve ninguna bicicleta. Debe llevar tono info, el título «Sin resultados», un texto que mencione el tipo filtrado con su etiqueta legible y un botón «Ver todas» que llame a una prop alRestablecer. Intégralo en App sin tocar ListaBicicletas.

Ejercicio 3. Convierte este HOC en composición explícita. Explica qué mejora y qué se pierde.

function conPanel(Componente, titulo) {
  return function ComponenteConPanel(props) {
    return (
      <Panel titulo={titulo}>
        <Componente {...props} />
      </Panel>
    );
  };
}

export default conPanel(ListaBicicletas, 'Catálogo');

Soluciones

Solución 1.

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

/**
 * Tarjeta de resumen con icono, cuerpo libre y acciones.
 * Props:
 *  - titulo   (cadena, obligatorio)
 *  - icono    (JSX, opcional)
 *  - children (contenido)
 *  - acciones (JSX, opcional)
 */
function TarjetaResumen({ titulo, icono, children, acciones }) {
  return (
    <article className={estilos.tarjeta}>
      <header className={estilos.encabezado}>
        {icono && <span className={estilos.icono} aria-hidden="true">{icono}</span>}
        <h3 className={estilos.titulo}>{titulo}</h3>
      </header>
      <div className={estilos.cuerpo}>{children}</div>
      {acciones && <footer className={estilos.acciones}>{acciones}</footer>}
    </article>
  );
}

export default TarjetaResumen;
// src/App.jsx (fragmento)
<TarjetaResumen titulo="Flota" icono="🚲">
  <ResumenFlota flota={bicicletas} />
</TarjetaResumen>

<TarjetaResumen
  titulo={estaciones[0].nombre}
  icono="📍"
  acciones={<button type="button" onClick={() => manejarVerEstacion(estaciones[0])}>Ver bicicletas</button>}
>
  <p>Barrio: {estaciones[0].barrio}</p>
  <p>Plazas: {estaciones[0].plazas}</p>
</TarjetaResumen>

El mismo contenedor sirve para dos contenidos que no tienen nada que ver, sin una línea condicional dentro: eso es composición funcionando.

Solución 2.

// src/componentes/AvisoSinResultados.jsx
import Aviso from './Aviso.jsx';

const ETIQUETAS = {
  todos: 'Todas',
  urbana: 'Urbanas',
  electrica: 'Eléctricas',
  carga: 'De carga'
};

/**
 * Props:
 *  - tipoElegido   (cadena, obligatorio)
 *  - alRestablecer (función, obligatorio)
 */
function AvisoSinResultados({ tipoElegido, alRestablecer }) {
  return (
    <Aviso
      tono="info"
      titulo="Sin resultados"
      acciones={<button type="button" onClick={alRestablecer}>Ver todas</button>}
    >
      No hay bicicletas del tipo <strong>{ETIQUETAS[tipoElegido]}</strong> en el catálogo
      ahora mismo.
    </Aviso>
  );
}

export default AvisoSinResultados;
// src/App.jsx (fragmento)
{bicicletasVisibles.length === 0 ? (
  <AvisoSinResultados
    tipoElegido={tipoElegido}
    alRestablecer={() => setTipoElegido('todos')}
  />
) : (
  <ListaBicicletas
    bicicletas={bicicletasVisibles}
    estaciones={estaciones}
    alSeleccionar={manejarSeleccionBicicleta}
    alReservar={manejarReserva}
  />
)}

ListaBicicletas no se toca: sigue con su propio estado vacío genérico para cuando la usen otras pantallas, y App decide aquí un mensaje más específico porque es quien conoce el filtro. Fíjate en que ETIQUETAS está duplicado en dos ficheros; si el proyecto crece, el sitio natural para esa constante sería src/datos/dominio.js.

Solución 3.

// Composición explícita: se usa directamente donde haga falta
<Panel titulo="Catálogo">
  <ListaBicicletas
    bicicletas={bicicletasVisibles}
    estaciones={estaciones}
    alSeleccionar={manejarSeleccionBicicleta}
    alReservar={manejarReserva}
  />
</Panel>

Qué mejora: el título deja de estar congelado en el momento de crear el componente y puede depender del estado —un titulo calculado con el número de resultados, por ejemplo—; desaparece un nivel del árbol y un nombre raro en las DevTools; y quien lee el JSX ve de un vistazo que hay un panel envolviendo la lista, sin abrir otro fichero.

Qué se pierde: si el mismo panel con el mismo título se repitiera en quince sitios, el HOC evitaría repetir dos líneas. La respuesta idiomática a eso no es el HOC, sino una especialización por configuración: function PanelCatalogo({ children }) { return <Panel titulo="Catálogo">{children}</Panel>; }, que se lee como JSX normal y conserva toda la flexibilidad.

Conclusión

React sustituye la herencia por la composición, y no le falta ninguna capacidad por ello. children cubre los contenedores de una sola zona (Panel, Modal, Diseno); los huecos con nombre cubren los de varias (cabecera, acciones, pie); la especialización por configuración sustituye a las subclases (AvisoMantenimiento a partir de Aviso); las render props resuelven el caso en que el contenedor aporta datos además de marcado, aunque hoy suelan ceder el sitio a un hook personalizado; y los componentes de orden superior se leen pero no se escriben, salvo para mantener código existente. La regla que ordena todo esto es una sola: componer para la interfaz, extraer funciones o hooks para la lógica.

La pantalla de catálogo de CicloUrbano ha quedado recompuesta con Diseno + PanelAvanzado + contenido, con los avisos saliendo todos de un Aviso genérico, y App vuelve a leerse como un índice.

Queda pendiente algo que las dos últimas lecciones han rozado sin abordar: los componentes nacen, cambian y mueren. Un Modal que se abre y se cierra, un panel de actividad que necesita un temporizador mientras está en pantalla y debe darlo de baja al desaparecer. Esa vida tiene fases, y durante años se gestionó con los métodos del ciclo de vida de los componentes de clase, que todavía hoy pueblan las bases de código reales. La próxima lección es Métodos del Ciclo de Vida de React, donde aprenderás a leerlos, a entender qué problema resolvían y a traducirlos al modelo mental que usa React moderno.

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