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
- Qué es un manejador de eventos y dónde vive
onClickfrente aonclick: las diferencias reales- Pasar la función, no llamarla
- El objeto de evento sintético
preventDefaultystopPropagation- Manejadores con argumentos: funciones flecha en línea
- Propagación y captura
- Catálogo de eventos habituales
- Dónde definir el manejador y cómo nombrarlo
- CicloUrbano: un selector que responde y una tarjeta que avisa
- 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.
onClickes una prop, no un atributo de HTML. Se escribe en camelCase, comoclassNameotabIndex, 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.
onClick frente a onclick: las diferencias reales
onClick frente a onclick: las diferencias realesSi 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 | Sí, 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)"]
- 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:
- El mensaje del
console.logaparece nada más cargar la página, sin que nadie haya pulsado. - Al pulsar el botón no pasa absolutamente nada, porque
onClickesundefined. - 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.
- 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 encuentrasevento.persist()en un tutorial, es código antiguo: en React 19 no hace falta y no hace nada.
preventDefault y stopPropagation
preventDefault y stopPropagationSon 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.
- 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:
Lo que pasa aquí, paso a paso:
() => seleccionar('urbana')crea una función nueva que no se ejecuta todavía.- Esa función es lo que se asigna a
onClick. - 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:
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.
- Propagación y captura
Cuando pulsas un elemento anidado, el evento no afecta solo a ese elemento: recorre el árbol en dos fases.
- Fase de captura: el evento desciende desde la raíz hasta el elemento pulsado.
- 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 Capture —onClickCapture, 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.
- 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:
onChangede React no es elchangedel 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.onMouseEnteryonMouseLeaveno burbujean, a diferencia deonMouseOveryonMouseOut. Por eso son los recomendados para efectos visuales: no se disparan al pasar por encima de los hijos.
- 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.
- 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é:
manejarSeleccionTipoes una función con nombre. Antes la lógica estaba dentro delonClick; 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 eltipode cada botón. Sin ella, la función se ejecutaría durante el render, como viste en el apartado 3.alCambiarTipoes 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>valetype="submit"por defecto y enviaría el formulario al pulsarlo. Es una precaución barata que evita un fallo desconcertante.onKeyDownen el contenedor: gracias al burbujeo, un solo manejador cubre los cuatro botones. Al pulsarEscapecon 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:
alSeleccionaryalReservarson props opcionales. La comprobaciónif (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
onClicken 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.
onclickno es una prop de React: React lo trata como un atributo desconocido y avisa por consola con un mensaje que sugiereonClick. 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 falsecancele el comportamiento por defecto. En React el valor de retorno se ignora: usaevento.preventDefault(). - Confundir
preventDefaultconstopPropagation. 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
targetconcurrentTarget. Si el elemento pulsable contiene un icono o un<span>,targetserá ese hijo. Para identificar el elemento del manejador, usacurrentTarget. - Olvidar
type="button"en botones dentro de un formulario. El valor por defecto essubmit, 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
onClicken 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,tabIndexy manejador de teclado, como verás en 03-06. - Consejo: un manejador, una responsabilidad. Si
manejarClicguarda estado, avisa al padre y registra analítica, extrae funciones auxiliares. - Consejo: aprovecha el burbujeo. Un solo
onKeyDownen 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
bicicletay dos props de función:alSeleccionaryalMarcarMantenimiento. - 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.tagNamedesde qué elemento se ha ejecutado cada manejador, para comprobar la diferencia contarget.
Ejercicio 3
Añade a SelectorTipo un comportamiento de teclado más completo, usando un único manejador en el contenedor:
Escapevuelve al tipo'todos'(ya está hecho).- La tecla
ArrowRightavanza al siguiente tipo de la constanteTIPOS, yArrowLeftretrocede, 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
- ¿Qué es React?
- Configuración del Entorno de Desarrollo
- Hola Mundo en React
- JSX: Extensión de Sintaxis de JavaScript
- Cómo Renderiza React: Virtual DOM y Reconciliación
Módulo 2: Componentes de React
- Entendiendo los Componentes
- Componentes Funcionales vs de Clase
- Props: Pasando Datos a Componentes
- State: Gestión del Estado del Componente
- Estilos en los Componentes: CSS, Módulos y Utilidades
Módulo 3: Trabajando con Eventos
- Manejo de Eventos en React
- Renderizado Condicional
- Listas y Claves
- Formularios y Componentes Controlados
- Validación de Formularios y Componentes No Controlados
- Accesibilidad en Componentes Interactivos
Módulo 4: Conceptos Avanzados de Componentes
- Elevando el Estado
- Composición vs Herencia
- Métodos del Ciclo de Vida de React
- Hooks: Introducción y Uso Básico
- Límites de Error: Capturar Fallos en la Interfaz
Módulo 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef y Acceso al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalizados
Módulo 6: Enrutamiento en React
- Introducción a React Router
- Configuración de React Router
- Rutas Anidadas
- Navegación Programática
- Rutas Protegidas y Control de Acceso
Módulo 7: Gestión del Estado
- Introducción a la Gestión del Estado
- API de Contexto
- Redux: Introducción y Configuración
- Redux: Acciones y Reductores
- Redux: Conectando a React
- Estado del Servidor: Peticiones, Caché y Sincronización
Módulo 8: Optimización del Rendimiento
- Técnicas de Optimización del Rendimiento en React
- Memorización con React.memo
- Hooks useMemo y useCallback
- División de Código y Carga Perezosa
- Medir el Rendimiento con React DevTools Profiler
Módulo 9: Pruebas en React
- Introducción a las Pruebas
- Pruebas Unitarias con Jest
- Pruebas de Componentes con React Testing Library
- Pruebas de Código Asíncrono y Simulación de APIs
- Pruebas de Extremo a Extremo con Cypress
Módulo 10: Temas Avanzados
- Renderizado del Lado del Servidor (SSR) con Next.js
- Generación de Sitios Estáticos (SSG) con Next.js
- Suspense y React Server Components
- TypeScript con React
- React Native: Creación de Aplicaciones Móviles
