Módulo 4: Events And Handlers
Presentación del módulo: la interfaz que responde
Por qué este módulo existe aquí
En el módulo 3 completamos la ecuación que sostiene toda la guía. Aprendiste que un componente tiene estado —su memoria propia, que persiste entre renders— y que al cambiar ese estado con su setter (setQuery, setCount), React vuelve a ejecutar el componente y actualiza la pantalla. Esa es la ecuación viva: UI = f(estado). Cambias el estado, y la interfaz sigue.
Pero vuelve a mirar cómo ejecutábamos aquellos ejemplos. En el mini-runtime de Node llamábamos a setCount nosotros mismos, a mano, desde el código de prueba: setCount(c => c + 1) escrito en el script, tres veces, para simular tres interacciones. La pantalla cambiaba, sí, pero porque el programador empujaba el setter. El usuario real —la persona que escribe en la barra de búsqueda, la que hace clic en "Add to cart"— no estaba en la ecuación. Faltaba el eslabón que conecta lo que hace una persona con el setter que se dispara. Sin ese eslabón, la interfaz solo se mueve si tú, desde el código, la mueves; y eso no es una interfaz, es una animación con guion.
Conexión con el módulo. Este módulo instala ese eslabón que faltaba: los eventos. Un evento es algo que ocurre en la interfaz por acción del usuario —un clic, una tecla, el envío de un formulario—. React te deja conectar una función a un evento; a esa función se le llama event handler (o manejador), y cuando el evento ocurre, React la ejecuta. Dentro del handler es donde por fin llamas al setter. La cadena completa, que es el corazón de este módulo, queda así:
usuario hace algo -> React dispara el evento -> tu handler corre ->
handler llama a setState -> el estado cambia -> React re-renderiza
Esta es la lección-mapa: no entramos a fondo en ninguna pieza todavía, sino que instalamos la idea (los eventos cierran el bucle de la interactividad), la mostramos ejecutada de punta a punta, y damos el mapa de cómo cada lección construye una parte. En este módulo trabajamos cuatro cosas: pasar la función y no llamarla (la trampa onClick={fn} vs onClick={fn()}), el objeto evento (e.target.value, e.preventDefault()), los inputs controlados (la SearchBar cuyo value sale del estado), y pasar datos hacia arriba por callbacks (el onAddToCart del ProductCard). Lo que no toca este módulo: dónde vive el estado compartido —el carrito que muchas piezas tocan— es levantar el estado, y eso es el módulo 7. Aquí el dato sube por un callback, pero el estado que lo recibe ya lo asumimos en el padre.
Y la promesa de siempre, que se cumple en cada lección: nada se cita de memoria, todo se ejecuta. React del navegador no se puede "correr y ver el DOM" aquí, así que hacemos lo honesto: la sintaxis JSX real con handlers se muestra en los bloques de código —es la API exacta que escribirás— y la lógica pura del flujo de un evento se ejecuta en Node con solo JavaScript. Modelamos un "clic" como lo que de verdad es por dentro —la invocación de la función que guardaste en onClick— y modelamos el objeto evento como el objeto normal que es. Cada salida en los bloques "Qué esperar" es la salida literal de correr el código con Node 18.
Una analogía: el timbre de la casa
Imagina el timbre de una casa. Hay un botón afuera, junto a la puerta. Y hay una acción: adentro suena un din-don. El botón por sí solo no hace nada; la acción por sí sola no se dispara nunca. Lo que hace útil al timbre es el cable que conecta el botón con la campana: cuando alguien presiona el botón, la campana suena. Tú, cuando instalas el timbre, no te quedas parado esperando para tocar la campana con la mano cada vez que llega una visita. Haces algo mucho mejor: conectas el botón a la campana una sola vez, y desde entonces cualquiera que presione dispara el sonido, sin que tú estés presente.
Un event handler en React es exactamente ese cable. El botón es el elemento del DOM (<button>, <input>). La campana es tu función handler (lo que quieres que pase). Y onClick={handleClick} es el cable: le dices a React "cuando alguien haga clic en este botón, suena esta función". Tú no te quedas vigilando el botón para reaccionar; conectas el handler una vez, y React se encarga de dispararlo cada vez que el usuario hace clic.
De esta imagen salen, ya, dos de las lecciones del módulo. La primera: cuando instalas el timbre, conectas la campana, no el sonido ya sonado. Sería absurdo hacer sonar la campana en el momento de instalar el cable y esperar que eso quedara "conectado" para después. Pues bien: onClick={handleClick} conecta la campana; onClick={handleClick()} hace sonar la campana ahí mismo, al instalar, y deja el cable conectado a la nada. Esa es la trampa de la lección 3, y ya la intuyes desde el timbre. La segunda: cuando la campana suena, a veces quieres saber quién tocó o cómo —esa información viene con el evento—; en React llega como el objeto evento, la lección 4. Guarda la imagen del timbre: conectas el botón a una acción, una vez, y la acción se dispara sola cuando el usuario actúa.
El caso que nos acompaña: el storefront de Mercado, ahora interactivo
Seguimos con el storefront de Mercado —el frontend de la tienda: la barra de búsqueda, la lista de productos, el carrito—. Hasta el módulo 3 era una foto que cambiaba solo si tú, desde el código, le pasabas props o empujabas un setter. En este módulo le conectamos los eventos, y por primera vez responde a un usuario de verdad. Dos piezas concentran todo el módulo:
- La
SearchBar: un input controlado. Su texto vive en el estadoquery; cada tecla que el usuario escribe disparaonChange, que actualizaquery; y el input muestra siempre lo que dice el estado. Es el bucle del que trata la lección 5. - El botón "Add to cart" del
ProductCard: cuando el usuario hace clic, elProductCardllama a un callback que recibió por props,onAddToCart(product), y le "sube" el producto alApp, que decide agregarlo al carrito. Es el flujo hacia arriba de la lección 6.
Recuerda los datos: un producto (Product) tiene id, name, priceCents (el precio en centavos, como entero: 2599), category e inStock. Al mostrarlo lo formateamos con formatPrice(2599) → "$25.99". Y recuerda el árbol del storefront, porque los eventos lo recorren en las dos direcciones:
flowchart TD
App[App] --> SearchBar[SearchBar]
App --> ProductList[ProductList]
App --> Cart[Cart]
ProductList --> PC1[ProductCard]
ProductList --> PC2[ProductCard]
PC1 -. onAddToCart product .-> App
SearchBar -. onChange query .-> App
Las flechas continuas son las props bajando (lo de siempre, módulo 1). Las flechas punteadas son la novedad del módulo: los datos subiendo por callbacks —el query que sube de la SearchBar, el product que sube del ProductCard—. Los datos bajan por props; los datos suben por llamadas a función. Esa simetría es la mitad del módulo.
Ejemplo trabajado: un clic que sube el contador del carrito
Nada convence como verlo pasar de punta a punta. Vamos a montar la cadena completa —usuario → evento → handler → setState → re-render— con la pieza más pequeña posible: un botón que, a cada clic, sube en uno el contador de items del carrito. Primero, el componente tal como lo escribirás en React de verdad. Esto es JSX real; obsérvalo:
function CartButton() {
const [count, setCount] = useState(0);
const handleClick = () => setCount((c) => c + 1); // el handler
return <button onClick={handleClick}>Cart ({count})</button>;
}
Léelo con la analogía del timbre. handleClick es la campana: la función que queremos que suene. onClick={handleClick} es el cable: conecta el clic del botón con esa función. Y dentro de la campana, setCount(c => c + 1) —el updater funcional que aprendiste en el módulo 3— sube el contador. Fíjate en que a onClick le pasamos handleClick sin paréntesis: le pasamos la función, no el resultado de llamarla. Esa distinción es la lección 3; por ahora tómala como dada.
Ahora, ¿cómo lo ejecutamos si no hay navegador? Con el mismo truco de toda la guía, ampliado con una idea nueva. Un "clic" del usuario, por dentro, no es magia: es React invocando la función que guardaste en onClick. Así que modelamos el clic como exactamente eso —buscar element.props.onClick y llamarlo— y reusamos el mini-runtime de estado del módulo 3 (una celda de memoria que persiste entre renders):
// Mini-runtime: una celda de estado (como en el modulo 3) + un boton con onClick.
// Modela el ciclo completo: click -> handler -> setState -> re-render.
let cell;
let firstRender = true;
function useState(initial) {
if (firstRender) cell = initial;
const setState = (next) => {
cell = typeof next === 'function' ? next(cell) : next;
render(); // cambiar el estado dispara un re-render
};
return [cell, setState];
}
// h(): modela lo que escribes en JSX. Guarda onClick como una FUNCION en props.
function h(tag, props, ...children) {
return { tag, props: props || {}, children: children.flat() };
}
// El componente: un contador del carrito con un boton "Add".
function CartButton() {
const [count, setCount] = useState(0);
const handleClick = () => setCount((c) => c + 1); // el handler
console.log(` render -> boton muestra: "Cart (${count})"`);
return h('button', { onClick: handleClick }, `Cart (${count})`);
}
let tree;
function render() {
tree = CartButton();
firstRender = false;
}
// dispatchClick(): modela el click del usuario. React busca el onClick y lo INVOCA.
function dispatchClick(element) {
console.log('CLICK ->');
element.props.onClick(); // pasar la funcion, no llamarla: aqui React la llama
}
console.log('=== La interfaz responde: click -> handler -> setState -> re-render ===\n');
render(); // primer render
dispatchClick(tree);
dispatchClick(tree);
dispatchClick(tree);
console.log(`\nEstado final: count = ${cell}`);
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== La interfaz responde: click -> handler -> setState -> re-render ===
render -> boton muestra: "Cart (0)"
CLICK ->
render -> boton muestra: "Cart (1)"
CLICK ->
render -> boton muestra: "Cart (2)"
CLICK ->
render -> boton muestra: "Cart (3)"
Estado final: count = 3
Lee la salida como la cadena de eventos que es. El primer render produjo un botón que dice "Cart (0)" —el estado inicial—. Después viene el primer CLICK ->: dispatchClick buscó la función guardada en onClick y la invocó. Esa función (handleClick) llamó a setCount(c => c + 1), que subió la celda a 1 y disparó un re-render; por eso, justo debajo del CLICK, aparece "Cart (1)". El segundo clic repite el ciclo y da "Cart (2)"; el tercero, "Cart (3)". El estado final es 3.
Detente en lo esencial: nosotros no llamamos a setCount ni una sola vez en el código de prueba. Lo único que hicimos fue "hacer clic" —invocar el onClick—; el setter lo llamó el handler, no nosotros. Ese es el eslabón nuevo. En el módulo 3, para llegar de 0 a 3 teníamos que escribir setCount tres veces a mano; aquí, el usuario "hace clic" tres veces y el handler se encarga del resto. La interfaz por fin responde a una acción, no a una instrucción escrita en el guion. Compáralo con el timbre: no tocamos la campana; presionamos el botón, y el cable hizo sonar la campana solo.
El mapa del módulo
Guarda esta ruta; es cómo cada lección construye una parte del eslabón "eventos":
Tema Leccion Idea clave
──────────────────────────────── ──────── ──────────────────────────────────────────
Responder a eventos L2 un handler es una FUNCION; onClick la
conecta; React la invoca al ocurrir el evento
Pasa la funcion, no la llames L3 onClick={fn} conecta; onClick={fn()} corre
en el render y deja onClick en undefined
El objeto evento L4 e.target.value (leer el input),
e.preventDefault() (frenar el navegador)
Inputs controlados L5 value={query} + onChange={setQuery};
el estado es la unica fuente de verdad
Datos hacia arriba (callbacks) L6 el padre da onAddToCart por props; el hijo
lo invoca; el dato sube, el padre decide
Handlers limpios L7 saca la logica pesada del JSX a una
funcion con nombre
──────────────────────────────── ──────── ──────────────────────────────────────────
Cablea el storefront L8 el mini-proyecto, ejecutado
La frontera: qué NO entra en este módulo
Saber la frontera te evita esperar cosas que llegan después.
- Dónde vive el estado compartido —levantar el estado— es el módulo 7. En este módulo, cuando el
ProductCardllama aonAddToCart(product), el dato sube al padre; pero cómo y dónde el padre guarda ese carrito, y cómo baja de nuevo a un componenteCarthermano, es levantar el estado (módulo 7). Aquí asumimos que el padre ya tiene su estado y su handler; nos concentramos en el paso del dato hacia arriba. - Derivar en vez de guardar —el
ProductListfiltrado por la búsqueda— es el módulo 5. En el mini-proyecto de este módulo verás que la lista se filtra por elqueryque sube de laSearchBar; ese filtrado es un adelanto. Por qué la lista filtrada se computa del estado y no se guarda en otro estado es la tesis del módulo 5. - El estado en sí —
useState, el snapshot, el updater, la inmutabilidad— fue el módulo 3. Aquí lo usamos como herramienta ya conocida: los handlers llaman a los setters que aprendiste allá. SisetCount(c => c + 1)te suena a chino, vuelve al módulo 3. - Y todo lo del ecosistema que los módulos anteriores ya delimitaron sigue igual: HTML/CSS a fondo →
web-fundamentals-html-css; Next.js/SSR →nextjs-app-router; estado global y datos del servidor →frontend-state-and-data; estilos/design systems →ui-systems-and-design-implementation.
Errores comunes
Creer que "conectar un handler" es "llamar una función". Qué pasa: se escribe onClick={handleClick()} pensando que así "se ejecuta el handler cuando se hace clic". Por qué pasa: en JavaScript normal, para usar una función la llamas con paréntesis; cuesta ver que aquí queremos pasarla, no usarla todavía. Cómo detectarlo: el handler corre solo, apenas se pinta el componente, sin que nadie haya hecho clic; y el clic real no hace nada. Cómo corregirlo: recuerda el timbre —conectas la campana, no la haces sonar al instalar—. onClick={handleClick} conecta; onClick={handleClick()} suena ahí mismo. La lección 3 lo demuestra ejecutando; por ahora, quédate con que a onClick le pasas el nombre de la función, sin paréntesis.
Esperar que el estado cambie sin un evento (o sin un setter). Qué pasa: alguien escribe count = count + 1 dentro de un handler, o cambia una variable normal, y espera que la pantalla se actualice. Por qué pasa: se olvida que la UI solo sigue al estado, y que el estado solo cambia con su setter. Cómo detectarlo: el handler corre (lo ves en un console.log) pero la pantalla no se mueve. Cómo corregirlo: dentro del handler, llama al setter (setCount(...)), no reasignes la variable. El evento es quién dispara; el setter es cómo cambia el estado; y el re-render es la consecuencia. Los tres tienen que estar.
Pensar que este módulo también resuelve dónde guardar el carrito. Qué pasa: al ver onAddToCart(product) subir el producto, alguien intenta ya, aquí, armar el carrito completo compartido entre ProductList y Cart, y se enreda con quién es dueño del estado. Por qué pasa: el flujo hacia arriba y el estado compartido se sienten como el mismo tema. Cómo detectarlo: te preguntas "¿pero dónde pongo el useState del carrito para que lo vean los dos?" y no encuentras un lugar cómodo. Cómo corregirlo: separa las dos preguntas. Este módulo responde cómo sube el dato (por un callback que el padre da por props). Dónde vive el estado que lo recibe —el ancestro común, App— es el módulo 7. Aquí basta con que el padre tenga un handler que reciba el dato; el diseño del estado compartido viene después.
Ejercicios
Ejercicio 1 — Nombra la cadena. Con tus palabras, escribe la cadena completa de lo que pasa desde que el usuario hace clic en el botón Cart (0) hasta que la pantalla muestra Cart (1), nombrando las cinco etapas (evento, handler, setter, cambio de estado, re-render). Usa la salida del ejemplo trabajado como evidencia.
Ver solución
- El usuario hace clic en el
<button>. Ese es el evento (click). En el modelo,dispatchClicklo representa: imprimeCLICK ->. - React invoca el handler conectado en
onClick: la funciónhandleClick. En el modelo,element.props.onClick(). - El handler llama al setter:
setCount(c => c + 1). Es lo único que hace el handler. - El estado cambia: la celda pasa de
0a1. El setter lo hace y, acto seguido, dispara el re-render. - React re-renderiza: vuelve a ejecutar
CartButtoncon el estado nuevo, y produce un botón que dice"Cart (1)". En la salida, la línearender -> boton muestra: "Cart (1)"aparece justo debajo delCLICK ->.
La evidencia clave es que en el código de prueba nunca escribimos setCount: solo "hicimos clic". El setter lo llamó el handler. Eso es el eslabón que este módulo agrega a UI = f(estado).
Ejercicio 2 — El botón sin cable. Supón que alguien escribe <button>Cart ({count})</button> sin onClick. Explica, con la analogía del timbre, qué le falta, y qué pasaría al hacer clic. Luego di qué cambia si se agrega onClick={handleClick}.
Ver solución
Al botón sin onClick le falta el cable: hay un botón (el pulsador) y puede haber una campana (la función handleClick definida en algún lado), pero nada los conecta. Al hacer clic, no pasa nada: React no tiene ninguna función registrada para ese evento en ese elemento, así que no invoca nada, el estado no cambia, y la pantalla se queda igual. Es como un timbre con el botón puesto pero sin cablear a la campana: lo presionas y hay silencio.
Al agregar onClick={handleClick} conectas el cable: le dices a React "cuando alguien haga clic en este botón, invoca handleClick". A partir de ahí, cada clic dispara el handler, que llama al setter, que cambia el estado, que re-renderiza. El botón cobra vida. Nota que pasas handleClick sin paréntesis —conectas la campana, no la haces sonar al instalar—.
Ejercicio 3 — ¿Dato que baja o dato que sube? Para cada situación del storefront, di si el dato baja (por props) o sube (por un callback), y por qué: (a) App le pasa a ProductCard el nombre del producto; (b) el usuario hace clic en "Add to cart" y el ProductCard le avisa a App qué producto agregar; (c) App le pasa a la SearchBar el texto actual de la búsqueda; (d) el usuario teclea en la SearchBar y esta le avisa a App el nuevo texto.
Ver solución
- (a) Baja.
Appconoce el dato (el nombre) y se lo pasa aProductCardpor una prop (name). Es el flujo de siempre: padre → hijo, de arriba hacia abajo. (Módulo 1.) - (b) Sube. El clic ocurre en el hijo, pero quien decide qué hacer con el carrito es el padre. El hijo no puede "alcanzar" al padre directamente; lo que hace es invocar un callback que el padre le dio por props (
onAddToCart(product)). El dato (el producto) viaja del hijo al padre por esa llamada. (Módulo 4, lección 6.) - (c) Baja. El texto de la búsqueda vive en el estado de
App;Appse lo pasa a laSearchBarpor la propquery. Padre → hijo. (Lección 5.) - (d) Sube. El tecleo ocurre en la
SearchBar(el hijo), pero el estadoqueryvive enApp(el padre). LaSearchBarinvoca el callbackonQueryChange(nuevoTexto)queApple dio, y el nuevo texto sube. (Lecciones 5 y 6.)
La regla general: los datos bajan por props; los datos suben por llamadas a callbacks. Las props son de solo lectura (el hijo no puede cambiarlas), así que la única forma que tiene un hijo de afectar al padre es pedírselo invocando una función que el padre le prestó.
Resumen y siguiente paso
En esta lección instalaste el eslabón que le faltaba a UI = f(estado): los eventos, y su pieza operativa, el event handler —una función que conectas a un evento (onClick={handleClick}) y que React ejecuta cuando el evento ocurre—. Viste, con la analogía del timbre, que conectar un handler es tender un cable entre un botón y una acción, una sola vez, para que la acción se dispare sola cuando el usuario actúa. Y lo mediste ejecutando la cadena completa: un botón que, a punta de "clics", sube el contador del carrito de 0 a 3 sin que nosotros llamáramos al setter ni una vez —lo llamó el handler—. Con esto, la interfaz por fin responde a un usuario, no a un guion.
Antes de avanzar deberías poder: nombrar las cinco etapas de la cadena (evento → handler → setter → cambio de estado → re-render); explicar con el timbre por qué se pasa la función y no se la llama; distinguir un dato que baja (props) de uno que sube (callback); y ubicar la frontera —qué es de este módulo (conectar y disparar eventos, subir datos) y qué es del módulo 7 (dónde vive el estado compartido)—.
La lección 2 toma la primera pieza del mapa y la clava: responder a eventos con onClick y los handlers. Vas a ver, ejecutado, qué es exactamente un handler —una función guardada en una prop del elemento—, cómo se verifica que ahí hay una función (con nombre incluido), y cómo un "clic" no es más que React invocando esa función. Ahí, con código corriendo, "conectar un handler" pasa de analogía a mecánica.
Recursos
- React, "Responding to Events" — react.dev/learn/responding-to-events. La página oficial que introduce los event handlers,
onClick, y la regla de pasar la función sin llamarla. El punto de partida de este módulo. En inglés. - React, "Adding Interactivity" — react.dev/learn/adding-interactivity. El índice de la sección que cubre eventos, estado y re-render; todo este módulo vive aquí, y enlaza con el módulo 3. En inglés.
- React, "Reacting to Input with State" — react.dev/learn/reacting-to-input-with-state. Cómo pensar la UI como una función del estado que responde a la entrada del usuario; el marco mental de este módulo. En inglés.
- MDN, "Introduction to events" — developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events. Qué es un evento en el navegador, qué son los handlers y cómo funciona el modelo de eventos que React envuelve. En inglés.