Al cerrar el módulo anterior quedó dicho: el catálogo de CicloUrbano es un escaparate. Los datos llegan, las tarjetas se pintan con su color y su distintivo de estado, pero la aplicación no reacciona a nada de lo que hace la persona que la usa. Falta el cable que une la interfaz con la lógica, y ese cable son los eventos. En esta lección aprenderás cómo React conecta un clic, una pulsación de tecla o un cambio de foco con una función tuya: la sintaxis exacta, el error clásico de llamar al manejador en lugar de pasarlo, qué es un evento sintético y qué API te ofrece, cómo pasar argumentos, cómo funcionan la propagación y la captura, y cómo nombrar los manejadores para que el código siga siendo legible dentro de seis meses. Al final, SelectorTipo dejará de ser una maqueta y TarjetaBicicleta avisará a su padre de qué bicicleta se ha pulsado.

Contenido

  1. Qué es un manejador de eventos y dónde vive
  2. onClick frente a onclick: las diferencias reales
  3. Pasar la función, no llamarla
  4. El objeto de evento sintético
  5. preventDefault y stopPropagation
  6. Manejadores con argumentos: funciones flecha en línea
  7. Propagación y captura
  8. Catálogo de eventos habituales
  9. Dónde definir el manejador y cómo nombrarlo
  10. CicloUrbano: un selector que responde y una tarjeta que avisa

  1. Qué es un manejador de eventos y dónde vive

Un manejador de eventos (event handler) es una función normal y corriente que React ejecuta cuando ocurre algo en la interfaz: un clic, una tecla, un envío de formulario. No tiene nada de especial: es una función de JavaScript declarada dentro del cuerpo de tu componente.

// src/componentes/BotonPrueba.jsx
function BotonPrueba() {
  // 1. Se declara una función dentro del componente
  function manejarClic() {
    console.log('Han pulsado el botón');
  }

  // 2. Se entrega a React a través de una prop del elemento
  return <button onClick={manejarClic}>Pulsa aquí</button>;
}

export default BotonPrueba;

Tres detalles que conviene fijar desde el primer minuto:

  • La función se declara dentro del componente, no fuera. Así puede leer las props y el estado de esa instancia concreta, que es justo lo que necesitará en cuanto haga algo útil.
  • onClick es una prop, no un atributo de HTML. Se escribe en camelCase, como className o tabIndex, porque JSX usa nombres de propiedad, no de atributo.
  • El valor va entre llaves, porque es una expresión JavaScript: la referencia a la función. Entre comillas sería una cadena de texto, y React avisaría con un error.

Este patrón —declarar la función arriba, referenciarla abajo— es el que verás en el 95 % del código React que te encuentres.

  1. onClick frente a onclick: las diferencias reales

Si vienes de JavaScript sin frameworks conoces dos formas de escuchar un clic: el atributo onclick="..." en el HTML y elemento.addEventListener('click', fn) en el script. React sustituye ambas por una tercera.

Aspecto HTML onclick="..." DOM addEventListener React onClick={...}
Escritura Todo en minúscula Nombre en minúscula como cadena camelCase: onClick, onMouseEnter
Valor Una cadena con código Una referencia a función Una referencia a función
Dónde se escribe En el marcado En el JavaScript, buscando el nodo En el marcado, pero es JavaScript
Cuántos escuchadores Uno; el segundo pisa al primero Todos los que quieras Uno por prop; para varios, una función que llame a varias
Hay que retirarlo No , con removeEventListener, o hay fuga de memoria No: React lo hace al desmontar
Ámbito de las variables Global (window) El del cierre donde se define El del componente: props y estado a mano
Necesita buscar el nodo No Sí (getElementById, querySelector) No: el manejador viaja con el elemento
Objeto de evento Nativo Nativo Sintético (envoltorio, ver apartado 4)

Fíjate en las dos filas más importantes. La primera es que en React el valor es una función, no una cadena de código: no hay evaluación de texto, el editor autocompleta y los errores de sintaxis se detectan al compilar. La segunda es que no tienes que retirar el escuchador: cuando el componente desaparece de la pantalla, React limpia por ti. Con addEventListener manual, olvidarse del removeEventListener es una de las fuentes de fugas de memoria más habituales.

Un matiz técnico que conviene saber, aunque no cambia cómo escribes el código: React no coloca un escuchador en cada <button>. Adjunta unos pocos escuchadores en el nodo raíz de la aplicación (el <div id="root"> que viste en la lección 01-03) y, cuando llega un evento, decide qué manejador tuyo debe ejecutarse. Esta técnica se llama delegación de eventos y es la que permite que tener mil botones en pantalla no cueste mil escuchadores.

flowchart LR
    A["Clic en el navegador"] --> B["Escuchador único<br/>en la raíz de React"]
    B --> C["React identifica el<br/>elemento del árbol"]
    C --> D["Ejecuta tu función<br/>manejarClic(evento)"]

  1. Pasar la función, no llamarla

Este es el error más repetido de quien empieza con eventos en React, y merece su propio apartado porque el síntoma despista mucho.

// CORRECTO: pasamos la referencia a la función. React la llamará cuando toque
<button onClick={manejarClic}>Reservar</button>

// INCORRECTO: la llamamos nosotros durante el render
<button onClick={manejarClic()}>Reservar</button>

¿Qué ocurre exactamente en la segunda línea? Recuerda que el JSX se evalúa mientras se renderiza. Los paréntesis ejecutan la función en ese preciso momento, y lo que se asigna a onClick no es la función, sino su valor de retorno. Como manejarClic no devuelve nada, onClick acaba valiendo undefined.

Consecuencias, en orden de aparición:

  1. El mensaje del console.log aparece nada más cargar la página, sin que nadie haya pulsado.
  2. Al pulsar el botón no pasa absolutamente nada, porque onClick es undefined.
  3. Si el manejador actualiza estado, se dispara un nuevo render, que vuelve a llamar a la función, que vuelve a actualizar el estado… bucle infinito y la pestaña se congela con un error de «Too many re-renders».

La regla mnemotécnica es simple: con paréntesis, ahora; sin paréntesis, después.

onClick={manejarClic}      // después (cuando se pulse)  ✔
onClick={manejarClic()}    // ahora (durante el render)  ✘
onClick={() => manejarClic()}  // después, envuelto en otra función  ✔

La tercera forma es válida y la necesitarás en el apartado 6, cuando haya que pasar argumentos. Pero si el manejador no lleva argumentos, escribe la primera: es más corta y más directa.

  1. El objeto de evento sintético

Cuando React ejecuta tu manejador, le pasa un argumento: el objeto del evento.

function manejarClic(evento) {
  console.log(evento.type);            // "click"
  console.log(evento.target);          // el nodo del DOM que recibió el clic
  console.log(evento.currentTarget);   // el nodo donde está el manejador
}

No es el evento nativo del navegador: es un evento sintético (SyntheticEvent), un objeto que React construye envolviendo al nativo.

Por qué existe

En los años en los que React nació, cada navegador exponía los eventos de forma ligeramente distinta: nombres de propiedad diferentes, comportamientos incompatibles, preventDefault que en algún navegador se llamaba de otra manera. El evento sintético resuelve tres cosas:

Motivo Qué aporta
Normalización La misma API en todos los navegadores; tu código no lleva condicionales por navegador
Compatibilidad con la delegación React puede reconstruir el recorrido del evento a través del árbol de componentes, que no siempre coincide con el árbol del DOM
Interfaz estable React garantiza qué propiedades existen, aunque el navegador cambie

La API sintética imita a la nativa, así que todo lo que sabes de eventos del DOM sigue valiendo. Las propiedades y métodos que usarás a diario:

Miembro Qué es
evento.target El elemento donde se originó el evento (el más profundo)
evento.currentTarget El elemento al que está adjunto el manejador que se está ejecutando
evento.type El tipo de evento como cadena: "click", "submit", "keydown"
evento.preventDefault() Cancela la acción por defecto del navegador
evento.stopPropagation() Impide que el evento siga subiendo por el árbol
evento.key En eventos de teclado, la tecla pulsada: "Enter", "Escape", "a"
evento.nativeEvent El evento nativo original, por si necesitas algo que el sintético no expone

La distinción entre target y currentTarget importa. Si un <button> contiene un <span> y pulsas sobre el texto, evento.target será el <span>, mientras que evento.currentTarget será el <button>. Cuando quieras identificar «el elemento al que puse el manejador», usa siempre currentTarget.

function BotonConIcono() {
  function manejarClic(evento) {
    console.log(evento.target.tagName);        // "SPAN" si pulsas el texto
    console.log(evento.currentTarget.tagName); // "BUTTON" siempre
  }

  return (
    <button onClick={manejarClic}>
      <span>Reservar</span>
    </button>
  );
}

Nota histórica útil: hasta React 16, los eventos sintéticos se reutilizaban (pooling): al terminar el manejador, sus propiedades se vaciaban, y para leerlas más tarde había que llamar a evento.persist(). Desde React 17 eso desapareció. Si encuentras evento.persist() en un tutorial, es código antiguo: en React 19 no hace falta y no hace nada.

  1. preventDefault y stopPropagation

Son dos métodos distintos que se confunden constantemente. Hacen cosas que no tienen nada que ver.

preventDefault(): cancelar lo que haría el navegador

Algunos elementos tienen un comportamiento nativo asociado: un <a href> navega, un <form> se envía y recarga la página, un <input type="checkbox"> se marca. preventDefault() cancela ese comportamiento.

function EnlaceEstacion() {
  function manejarClic(evento) {
    evento.preventDefault();   // el navegador NO navega a /estaciones
    console.log('Mostrar el detalle sin cambiar de página');
  }

  return (
    <a href="/estaciones" onClick={manejarClic}>
      Ver estaciones
    </a>
  );
}

En React hay un detalle importante: devolver false desde el manejador no cancela nada. En HTML clásico, onclick="return false" sí lo hacía. En React el valor de retorno se ignora por completo: hay que llamar explícitamente a evento.preventDefault().

Su uso más frecuente, con diferencia, es el envío de formularios, que verás a fondo en Formularios y Componentes Controlados.

stopPropagation(): cortar el ascenso del evento

Impide que el evento siga subiendo hacia los elementos antepasados. Es la herramienta para el caso «tengo un botón dentro de una tarjeta pulsable y no quiero que pulsar el botón active también la tarjeta».

function TarjetaConBoton() {
  function manejarClicTarjeta() {
    console.log('Se ha seleccionado la tarjeta');
  }

  function manejarClicBoton(evento) {
    evento.stopPropagation();   // sin esto, también se ejecutaría manejarClicTarjeta
    console.log('Se ha pulsado el botón de reservar');
  }

  return (
    <article onClick={manejarClicTarjeta}>
      <h3>Urbana Clásica</h3>
      <button onClick={manejarClicBoton}>Reservar</button>
    </article>
  );
}
Método Qué cancela Ejemplo típico
preventDefault() La acción por defecto del navegador Que un <form> recargue la página
stopPropagation() La subida del evento a los antepasados Un botón dentro de una tarjeta pulsable

Los dos son independientes: puedes llamar a uno, al otro, a ambos o a ninguno.

  1. Manejadores con argumentos: funciones flecha en línea

Muy a menudo el manejador necesita saber sobre qué se ha actuado: qué tipo de bicicleta, qué identificador, qué fila. Y ahí choca con la regla del apartado 3: no podemos escribir onClick={seleccionar('urbana')} porque eso lo ejecutaría durante el render.

La solución idiomática es envolverlo en una función flecha en línea:

<button onClick={() => seleccionar('urbana')}>Urbanas</button>

Lo que pasa aquí, paso a paso:

  1. () => seleccionar('urbana') crea una función nueva que no se ejecuta todavía.
  2. Esa función es lo que se asigna a onClick.
  3. Cuando alguien pulsa, React llama a esa función, y entonces se ejecuta seleccionar('urbana').

Si además necesitas el objeto de evento, decláralo como parámetro de la flecha y pásalo:

<button onClick={(evento) => seleccionar('urbana', evento)}>Urbanas</button>

La alternativa: una función que devuelve una función

Existe un patrón equivalente, algo más sofisticado, llamado currificación:

function ListaTipos() {
  // Recibe el tipo y DEVUELVE el manejador ya preparado
  const alSeleccionarTipo = (tipo) => (evento) => {
    console.log('Tipo elegido:', tipo, 'desde', evento.currentTarget.textContent);
  };

  return (
    <>
      <button onClick={alSeleccionarTipo('urbana')}>Urbanas</button>
      <button onClick={alSeleccionarTipo('electrica')}>Eléctricas</button>
    </>
  );
}

Aquí alSeleccionarTipo('urbana') sí se ejecuta durante el render, pero no pasa nada: lo que devuelve es otra función, y esa es la que se asigna a onClick. Es correcto, aunque para principiantes la flecha en línea es más fácil de leer.

El coste de crear la función en cada render

Cada vez que el componente se renderiza, la expresión () => seleccionar('urbana') produce una función nueva. Es un objeto distinto del de la vez anterior, aunque haga exactamente lo mismo.

¿Es un problema? En la inmensa mayoría de los casos, no. Crear una función en JavaScript es una operación baratísima, y la legibilidad que ganas compensa de sobra. Solo empieza a importar cuando esa función se pasa como prop a un componente memorizado, porque entonces la nueva referencia rompe la memorización y provoca renders innecesarios.

Ese escenario, cómo medirlo y cómo resolverlo con useCallback es material del Módulo 8, concretamente de Hooks useMemo y useCallback. Por ahora quédate con la regla práctica: escribe flechas en línea sin miedo y optimiza solo cuando midas un problema real.

  1. Propagación y captura

Cuando pulsas un elemento anidado, el evento no afecta solo a ese elemento: recorre el árbol en dos fases.

  1. Fase de captura: el evento desciende desde la raíz hasta el elemento pulsado.
  2. Fase de burbujeo (bubbling): el evento asciende desde el elemento pulsado hasta la raíz.

React te permite escuchar en ambas. Por defecto, onClick escucha en el burbujeo. Añadiendo el sufijo CaptureonClickCapture, onFocusCapture, onKeyDownCapture— escuchas en la captura.

flowchart TD
    subgraph CAPTURA["Fase 1: captura (de fuera hacia dentro)"]
        direction TB
        C1["main<br/>onClickCapture"] --> C2["article.tarjeta<br/>onClickCapture"] --> C3["button<br/>onClickCapture"]
    end
    C3 --> OBJ["Elemento pulsado:<br/>button"]
    OBJ --> B1
    subgraph BURBUJEO["Fase 2: burbujeo (de dentro hacia fuera)"]
        direction TB
        B1["button<br/>onClick"] --> B2["article.tarjeta<br/>onClick"] --> B3["main<br/>onClick"]
    end

Veámoslo con un ejemplo que imprime el recorrido completo:

function DemostracionPropagacion() {
  return (
    <main
      onClickCapture={() => console.log('1. main - captura')}
      onClick={() => console.log('6. main - burbujeo')}
    >
      <article
        onClickCapture={() => console.log('2. article - captura')}
        onClick={() => console.log('5. article - burbujeo')}
      >
        <button
          onClickCapture={() => console.log('3. button - captura')}
          onClick={() => console.log('4. button - burbujeo')}
        >
          Pulsa
        </button>
      </article>
    </main>
  );
}

Al pulsar el botón, la consola imprime los seis mensajes exactamente en ese orden: 1, 2, 3, 4, 5, 6.

Para qué sirve cada fase

Fase Cuándo se usa
Burbujeo (onClick) El 99 % de los casos. Es el comportamiento natural que esperas
Burbujeo en el padre Un solo manejador en el contenedor para muchos hijos, identificando el origen con evento.target
Captura (onClickCapture) Interceptar antes que los hijos: registro de analítica, cerrar un menú desplegable, bloquear la interacción en una zona

Un detalle importante: stopPropagation() corta el recorrido desde donde se llame. Si lo llamas en la fase de captura de article, el botón ni siquiera llegará a recibir su propio clic. Úsalo con cuidado: cortar la propagación puede romper funcionalidades de otros componentes que dependían de recibir ese evento.

  1. Catálogo de eventos habituales

React soporta prácticamente todos los eventos del DOM, siempre en camelCase. Estos son los que usarás constantemente:

Prop Cuándo se dispara Notas prácticas
onClick Al pulsar (ratón, o Enter/Espacio en un <button>) El más usado. En un <button> real funciona también con teclado
onChange Al cambiar el valor de un campo En React se dispara en cada pulsación de tecla, no al perder el foco como en el DOM nativo
onInput En cada entrada de texto En React es casi idéntico a onChange; usa onChange por convención
onSubmit Al enviar un <form> Va en el formulario, no en el botón. Casi siempre con preventDefault()
onFocus Al recibir el foco un campo Útil para resaltar o mostrar ayuda contextual
onBlur Al perder el foco El momento clásico para validar un campo (lo verás en 03-05)
onKeyDown Al pulsar una tecla Se lee con evento.key. Es el que permite reaccionar a Enter o Escape
onKeyUp Al soltar una tecla Menos habitual; para atajos, prefiere onKeyDown
onMouseEnter Al entrar el ratón en el elemento No burbujea: ideal para efectos de un solo elemento
onMouseLeave Al salir el ratón Pareja natural del anterior
onDoubleClick Al hacer doble clic Ojo con la accesibilidad: no hay equivalente con teclado
onContextMenu Al abrir el menú contextual Requiere preventDefault() para sustituirlo
onScroll Al desplazar un contenedor Se dispara mucho; conviene limitar su frecuencia

Dos advertencias sobre esta tabla:

  • onChange de React no es el change del DOM. El nativo solo avisa cuando el campo pierde el foco tras haber cambiado; el de React avisa en cada tecla. Esta diferencia es la que hace posibles los componentes controlados de 03-04.
  • onMouseEnter y onMouseLeave no burbujean, a diferencia de onMouseOver y onMouseOut. Por eso son los recomendados para efectos visuales: no se disparan al pasar por encima de los hijos.

  1. Dónde definir el manejador y cómo nombrarlo

Con tres manejadores el código todavía se lee; con quince, la disciplina de nombres marca la diferencia entre un componente mantenible y un jeroglífico.

La convención del curso

Caso Convención Ejemplo
Función definida dentro del componente Prefijo manejar + lo que ocurre manejarClic, manejarEnvio, manejarCambioTipo
Prop de función que un componente recibe Prefijo al (o on en proyectos en inglés) alSeleccionar, alReservar, alCambiarTipo
Manejador de un elemento del DOM La prop nativa, sin inventar onClick, onSubmit, onKeyDown

La idea de fondo es la simetría: el hijo declara qué le puede pasar (alSeleccionar) y el padre decide qué hacer cuando pasa (manejarSeleccion).

// El padre define la función y la pasa hacia abajo
function Catalogo() {
  function manejarSeleccion(bicicleta) {
    console.log('Bicicleta seleccionada:', bicicleta.modelo);
  }

  return <TarjetaBicicleta bicicleta={primeraBici} alSeleccionar={manejarSeleccion} />;
}

// El hijo solo sabe que debe avisar; no sabe qué pasará después
function TarjetaBicicleta({ bicicleta, alSeleccionar }) {
  return <article onClick={() => alSeleccionar(bicicleta)}>…</article>;
}

Este patrón se llama elevar el manejador y es la base de la comunicación hijo → padre en React: las props bajan datos, las props de función suben avisos.

Cuándo definir la función fuera del JSX

Situación Recomendación
El manejador tiene una sola línea y no lleva argumentos Función nombrada arriba: onClick={manejarClic}
El manejador necesita un argumento del contexto Flecha en línea: onClick={() => manejarClic(tipo)}
El manejador tiene más de dos o tres líneas Siempre función nombrada arriba: el JSX debe leerse como marcado
El manejador no depende del componente Función auxiliar en src/utilidades/, importada

La regla que resume todo: el JSX describe la interfaz, no la lógica. Si dentro de un onClick hay un if, un try o tres sentencias, esa lógica pertenece a una función con nombre.

  1. CicloUrbano: un selector que responde y una tarjeta que avisa

Vamos a aplicar todo esto a la aplicación. Dos cambios concretos.

SelectorTipo con manejadores de verdad

La versión de 02-04 ya usaba onClick, pero con la lógica escrita directamente en el JSX. Ahora que sabemos nombrar y organizar manejadores, queda así:

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

const TIPOS = ['todos', 'urbana', 'electrica', 'carga'];

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

/**
 * Selector del tipo de bicicleta.
 * Props:
 *  - alCambiarTipo (función, opcional): recibe el tipo elegido cuando cambia
 *
 * Sigue guardando el tipo en su propio estado; en 04-01 ese estado subirá al padre.
 */
function SelectorTipo({ alCambiarTipo }) {
  const [tipoElegido, setTipoElegido] = useState('todos');

  function manejarSeleccionTipo(tipo) {
    setTipoElegido(tipo);
    if (alCambiarTipo) {
      alCambiarTipo(tipo);
    }
  }

  function manejarTeclaLimpiar(evento) {
    if (evento.key === 'Escape') {
      manejarSeleccionTipo('todos');
    }
  }

  return (
    <div className={estilos.selector} onKeyDown={manejarTeclaLimpiar}>
      <p className={estilos.titulo}>Filtrar por tipo:</p>

      {TIPOS.map((tipo) => (
        <button
          key={tipo}
          type="button"
          className={clases(estilos.boton, tipo === tipoElegido && estilos.activo)}
          onClick={() => manejarSeleccionTipo(tipo)}
        >
          {ETIQUETAS[tipo]}
        </button>
      ))}

      <p className={estilos.seleccion}>
        Selección actual: <strong>{ETIQUETAS[tipoElegido]}</strong>
      </p>
    </div>
  );
}

export default SelectorTipo;

Qué ha cambiado y por qué:

  • manejarSeleccionTipo es una función con nombre. Antes la lógica estaba dentro del onClick; ahora hace dos cosas (guardar el estado y avisar al padre), y eso ya no cabe cómodamente en el JSX.
  • onClick={() => manejarSeleccionTipo(tipo)}: flecha en línea porque hace falta pasar el tipo de cada botón. Sin ella, la función se ejecutaría durante el render, como viste en el apartado 3.
  • alCambiarTipo es una prop de función opcional. Si el padre la pasa, el selector le avisa; si no, el componente sigue funcionando. Es la preparación para 04-01, donde el estado subirá al padre y el filtro funcionará de verdad.
  • type="button" en los botones. Sin él, un <button> dentro de un <form> vale type="submit" por defecto y enviaría el formulario al pulsarlo. Es una precaución barata que evita un fallo desconcertante.
  • onKeyDown en el contenedor: gracias al burbujeo, un solo manejador cubre los cuatro botones. Al pulsar Escape con el foco en cualquiera de ellos, el filtro vuelve a «Todas».

TarjetaBicicleta avisa de la bicicleta pulsada

// 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 el objeto bicicleta al pulsar la tarjeta
 *  - alReservar (función, opcional): recibe el objeto bicicleta al pulsar «Reservar»
 */
function TarjetaBicicleta({
  bicicleta,
  nombreEstacion = 'Estación desconocida',
  alSeleccionar,
  alReservar
}) {
  const precioFormateado = bicicleta.precioHora.toFixed(2).replace('.', ',');
  const noDisponible = bicicleta.estado !== 'disponible';

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

  function manejarClicReservar(evento) {
    // Sin esto, el clic subiría hasta el <article> y también seleccionaría la tarjeta
    evento.stopPropagation();
    if (alReservar) {
      alReservar(bicicleta);
    }
  }

  return (
    <article
      className={clases(estilos.tarjeta, estilos[bicicleta.tipo], noDisponible && 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}</p>
      <p className={estilos.precio}>{precioFormateado} € / hora</p>

      <button type="button" className={estilos.accion} onClick={manejarClicReservar}>
        Reservar
      </button>
    </article>
  );
}

export default TarjetaBicicleta;

Los dos puntos clave:

  • alSeleccionar y alReservar son props opcionales. La comprobación if (alSeleccionar) evita el error «alSeleccionar is not a function» cuando el padre no las pasa. Una alternativa igual de válida es dar un valor por defecto: alSeleccionar = () => {}.
  • evento.stopPropagation() en el botón es imprescindible: el botón está dentro del <article> que ya escucha clics, así que sin cortar la propagación cada «Reservar» dispararía también la selección de la tarjeta. Este es el caso de uso canónico del apartado 5.

Un apunte que retomaremos en Accesibilidad en Componentes Interactivos: poner onClick en un <article> lo hace pulsable con el ratón, pero no con el teclado. Es una deuda consciente que saldaremos al cerrar el módulo.

App recibe los avisos

// src/App.jsx
import { bicicletas } from './datos/dominio.js';
import Cabecera from './componentes/Cabecera.jsx';
import ResumenFlota from './componentes/ResumenFlota.jsx';
import SelectorTipo from './componentes/SelectorTipo.jsx';
import ListaBicicletas from './componentes/ListaBicicletas.jsx';
import PieDePagina from './componentes/PieDePagina.jsx';

function App() {
  function manejarCambioTipo(tipo) {
    console.log('Tipo elegido en el selector:', tipo);
  }

  function manejarSeleccionBicicleta(bicicleta) {
    console.log('Seleccionada:', bicicleta.id, bicicleta.modelo);
  }

  function manejarReserva(bicicleta) {
    console.log('Reservar:', bicicleta.id, 'a', bicicleta.precioHora, '€/hora');
  }

  return (
    <>
      <Cabecera />
      <main>
        <ResumenFlota flota={bicicletas} />
        <SelectorTipo alCambiarTipo={manejarCambioTipo} />
        <ListaBicicletas
          primera={bicicletas[0]}
          segunda={bicicletas[1]}
          tercera={bicicletas[2]}
          alSeleccionar={manejarSeleccionBicicleta}
          alReservar={manejarReserva}
        />
      </main>
      <PieDePagina />
    </>
  );
}

export default App;

ListaBicicletas se limita a reenviar las dos props de función a cada tarjeta. Por ahora App solo hace console.log: guardar de verdad la bicicleta seleccionada exige que el estado viva en el padre, y eso es exactamente el tema de Elevando el Estado. Aquí lo que importa es que el flujo ya funciona: el clic ocurre en la tarjeta y la información llega arriba.

flowchart TD
    APP["App<br/>manejarSeleccionBicicleta"] -- "alSeleccionar (prop función)" --> LIS["ListaBicicletas"]
    LIS -- "alSeleccionar" --> TAR["TarjetaBicicleta"]
    TAR -. "aviso al pulsar: alSeleccionar(bicicleta)" .-> APP

Abre la consola del navegador, pulsa una tarjeta y luego su botón «Reservar»: verás un mensaje distinto en cada caso, sin que se mezclen. CicloUrbano ya responde.

Errores Comunes y Consejos

  • Llamar al manejador en lugar de pasarlo. onClick={manejarClic()} lo ejecuta durante el render. Si además actualiza estado, provocas un bucle infinito y el error «Too many re-renders». Sin paréntesis, o envuelto en una flecha.
  • Escribir el evento en minúsculas. onclick no es una prop de React: React lo trata como un atributo desconocido y avisa por consola con un mensaje que sugiere onClick. Siempre camelCase.
  • Pasar el manejador como cadena. onClick="manejarClic()" es sintaxis de HTML; en JSX asigna la cadena de texto y no funciona.
  • Esperar que return false cancele el comportamiento por defecto. En React el valor de retorno se ignora: usa evento.preventDefault().
  • Confundir preventDefault con stopPropagation. El primero cancela lo que haría el navegador; el segundo, la subida del evento. No son intercambiables.
  • Abusar de stopPropagation(). Cortar la propagación «por si acaso» rompe funcionalidades que dependen del burbujeo, como cerrar un menú al pulsar fuera. Úsalo solo cuando tengas un conflicto real.
  • Confundir target con currentTarget. Si el elemento pulsable contiene un icono o un <span>, target será ese hijo. Para identificar el elemento del manejador, usa currentTarget.
  • Olvidar type="button" en botones dentro de un formulario. El valor por defecto es submit, así que el botón enviará el formulario y la página se recargará.
  • Usar evento.persist(). Es código de React 16. Desde React 17 los eventos ya no se reutilizan y esa llamada sobra.
  • Poner onClick en un <div> o un <article>. Funciona con el ratón y falla con el teclado. Cuando puedas, usa un <button>; si no, tendrás que añadir rol, tabIndex y manejador de teclado, como verás en 03-06.
  • Consejo: un manejador, una responsabilidad. Si manejarClic guarda estado, avisa al padre y registra analítica, extrae funciones auxiliares.
  • Consejo: aprovecha el burbujeo. Un solo onKeyDown en el contenedor cubre todos sus hijos. Menos código y menos posibilidades de olvidar un caso.
  • Consejo: comprueba con console.log(evento) cuando algo no cuadre. El objeto sintético es inspeccionable y suele revelar de inmediato si el problema es el elemento equivocado o el evento equivocado.

Ejercicios

Ejercicio 1

Analiza este componente e identifica tres errores relacionados con eventos. Explica el síntoma de cada uno y escribe la versión corregida.

function BotonEstacion({ estacion }) {
  function manejarClic(evento) {
    console.log('Estación:', estacion.nombre);
    return false;   // para que el enlace no navegue
  }

  return (
    <div>
      <a href="/estaciones" onclick={manejarClic()}>
        {estacion.nombre}
      </a>
    </div>
  );
}

Ejercicio 2

Crea el componente FilaBicicleta (src/componentes/FilaBicicleta.jsx) como una versión compacta de la tarjeta, pensada para una tabla. Debe:

  • Recibir bicicleta y dos props de función: alSeleccionar y alMarcarMantenimiento.
  • Pintar una fila <tr> con el modelo, el tipo, el estado y el precio, más una celda con un botón «Enviar a taller».
  • Avisar con alSeleccionar(bicicleta) cuando se pulse cualquier parte de la fila.
  • Avisar con alMarcarMantenimiento(bicicleta) cuando se pulse el botón, sin disparar además la selección de la fila.
  • Registrar en consola con evento.currentTarget.tagName desde qué elemento se ha ejecutado cada manejador, para comprobar la diferencia con target.

Ejercicio 3

Añade a SelectorTipo un comportamiento de teclado más completo, usando un único manejador en el contenedor:

  • Escape vuelve al tipo 'todos' (ya está hecho).
  • La tecla ArrowRight avanza al siguiente tipo de la constante TIPOS, y ArrowLeft retrocede, ambas de forma circular (del último al primero y viceversa).
  • Cualquier otra tecla no debe hacer nada.

Escribe solo el manejador y explica por qué te conviene calcular el nuevo índice a partir de TIPOS.indexOf(tipoElegido).

Soluciones

Solución 1.

Los tres errores:

Error Síntoma
onclick en minúscula React no reconoce la prop; el manejador nunca se ejecuta y aparece un aviso en consola sugiriendo onClick
manejarClic() con paréntesis La función se ejecuta durante el render: el mensaje aparece al cargar la página y onClick acaba valiendo undefined
return false En React el valor de retorno se ignora: el enlace navega igualmente y la página se recarga. Hay que llamar a evento.preventDefault()
// src/componentes/BotonEstacion.jsx

/**
 * Enlace a la ficha de una estación.
 * Props:
 *  - estacion (objeto, obligatorio) { id, nombre, barrio, plazas }
 */
function BotonEstacion({ estacion }) {
  function manejarClic(evento) {
    evento.preventDefault();   // cancela la navegación del navegador
    console.log('Estación:', estacion.nombre);
  }

  return (
    <div>
      <a href="/estaciones" onClick={manejarClic}>
        {estacion.nombre}
      </a>
    </div>
  );
}

export default BotonEstacion;

Solución 2.

// src/componentes/FilaBicicleta.jsx
import EtiquetaEstado from './EtiquetaEstado.jsx';

/**
 * Fila compacta de una bicicleta, para vistas de tabla.
 * Props:
 *  - bicicleta (objeto, obligatorio)
 *  - alSeleccionar (función, opcional): recibe la bicicleta al pulsar la fila
 *  - alMarcarMantenimiento (función, opcional): recibe la bicicleta al pulsar el botón
 */
function FilaBicicleta({ bicicleta, alSeleccionar, alMarcarMantenimiento }) {
  const precioFormateado = bicicleta.precioHora.toFixed(2).replace('.', ',');

  function manejarClicFila(evento) {
    console.log('Manejador en:', evento.currentTarget.tagName); // siempre "TR"
    console.log('Origen real:', evento.target.tagName);         // "TD", "SPAN"…
    if (alSeleccionar) {
      alSeleccionar(bicicleta);
    }
  }

  function manejarClicTaller(evento) {
    evento.stopPropagation();   // evita que el clic suba hasta el <tr>
    console.log('Manejador en:', evento.currentTarget.tagName); // "BUTTON"
    if (alMarcarMantenimiento) {
      alMarcarMantenimiento(bicicleta);
    }
  }

  return (
    <tr onClick={manejarClicFila}>
      <td>{bicicleta.modelo}</td>
      <td>{bicicleta.tipo}</td>
      <td>
        <EtiquetaEstado estado={bicicleta.estado} />
      </td>
      <td>{precioFormateado} €/h</td>
      <td>
        <button type="button" onClick={manejarClicTaller}>
          Enviar a taller
        </button>
      </td>
    </tr>
  );
}

export default FilaBicicleta;

Al pulsar el texto de una celda, currentTarget es siempre TR (donde está el manejador) mientras que target es TD. Al pulsar el botón, el stopPropagation() impide que se ejecute manejarClicFila: en consola solo aparece el mensaje del botón.

Solución 3.

function manejarTecla(evento) {
  const indiceActual = TIPOS.indexOf(tipoElegido);

  if (evento.key === 'Escape') {
    manejarSeleccionTipo('todos');
    return;
  }

  if (evento.key === 'ArrowRight') {
    const siguiente = (indiceActual + 1) % TIPOS.length;
    manejarSeleccionTipo(TIPOS[siguiente]);
    return;
  }

  if (evento.key === 'ArrowLeft') {
    // Sumar TIPOS.length antes del módulo evita índices negativos
    const anterior = (indiceActual - 1 + TIPOS.length) % TIPOS.length;
    manejarSeleccionTipo(TIPOS[anterior]);
  }

  // Cualquier otra tecla: no hacemos nada y el evento sigue su curso
}

Calcular el índice a partir de TIPOS.indexOf(tipoElegido) en lugar de guardar un estado indice aparte evita tener dos fuentes de verdad que podrían desincronizarse: el índice es un valor derivado del estado, exactamente como aprendiste en la lección 02-04. Y el operador módulo % da el comportamiento circular sin ningún if adicional; el truco de sumar TIPOS.length antes del módulo es necesario porque en JavaScript -1 % 4 vale -1, no 3.

Conclusión

Los eventos son el punto donde la interfaz deja de ser una imagen y se convierte en una aplicación. Ya sabes que un manejador es una función normal declarada dentro del componente y entregada a React mediante una prop en camelCase; que la prop recibe la función, no su llamada, y por qué unos paréntesis de más provocan desde un mensaje prematuro hasta un bucle infinito de renders; que React te entrega un evento sintético que normaliza la API entre navegadores, con target, currentTarget, preventDefault() y stopPropagation() como piezas de uso diario; que para pasar argumentos se envuelve el manejador en una flecha en línea, cuyo coste solo importará cuando llegues al Módulo 8; y que el evento recorre el árbol en dos fases, con onClick escuchando en el burbujeo y onClickCapture en la captura.

En CicloUrbano eso se ha traducido en dos piezas que ya responden: un SelectorTipo con manejadores nombrados, botones que resaltan la selección y atajos de teclado; y una TarjetaBicicleta que acepta las props de función alSeleccionar y alReservar para avisar hacia arriba de lo que ocurre dentro, con el stopPropagation() justo donde hacía falta. Los datos bajan por props; los avisos suben por props de función. Ese doble sentido es el flujo de datos de React, y no cambiará en todo el curso.

Ahora la aplicación reacciona, pero sigue pintando siempre lo mismo. Una bicicleta en mantenimiento no debería ofrecer un botón «Reservar», un catálogo sin resultados debería decirlo en lugar de quedarse en blanco, y un aviso solo debería aparecer cuando hay algo que avisar. Pintar unas cosas u otras según el estado o las props es el siguiente paso, y tiene más matices de los que parece —incluida una trampa con el operador && que ya asomó en la lección de JSX—. Lo verás en Renderizado Condicional.

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