Módulo 4: Events And Handlers

Responder a eventos: `onClick` y los handlers

Descripción

La lección 1 instaló la idea grande: los eventos son el eslabón que conecta al usuario con el estado. Esta lección clava la mecánica base de ese eslabón. Vas a ver, con precisión y con código corriendo, qué es exactamente un event handler, cómo se conecta a un elemento del DOM, y qué significa —literalmente, en memoria— que React "responda a un evento".

Conexión con el módulo. Es la primera pieza del mapa: responder a eventos con onClick. Todo lo que sigue en el módulo —pasar la función y no llamarla (L3), el objeto evento (L4), los inputs controlados (L5), los callbacks hacia arriba (L6)— se apoya en esta base: un handler es una función, se conecta pasándola como prop del elemento, y React la invoca cuando el evento ocurre. Si esto queda firme, el resto del módulo son variaciones sobre el mismo tema. Aquí seguimos con onClick (el clic de un botón) porque es el evento más simple; onChange y onSubmit son iguales en mecánica y llegan en las lecciones 4 y 5.

Una analogía: el timbre conectado a una acción

Volvamos al timbre de la lección 1, porque aquí lo desarmamos pieza por pieza. Un timbre tiene tres partes: el botón (el pulsador junto a la puerta), la acción (el din-don que suena adentro) y el cable que los une. Las tres son distintas y las tres hacen falta. El botón sin cable es un adorno: lo presionas y no pasa nada. La acción sin cable no se dispara nunca. El cable sin botón ni acción no conecta nada. Solo cuando el cable une el botón con la acción, presionar dispara el sonido.

En React, el botón es el elemento (<button>), la acción es tu función handler, y el cable es la prop del evento (onClick). Escribir onClick={handleAddClick} es, literalmente, tender el cable: le dices a React "el botón de este elemento va conectado a la función handleAddClick". A partir de ahí, cuando el usuario presiona (hace clic), React recorre el cable y ejecuta la acción.

Y hay un detalle de la analogía que importa para toda la lección: el cable guarda una referencia a la campana, no un sonido. Cuando conectas el timbre, no metes por el cable un din-don ya sonado; conectas el mecanismo que podrá sonar después. En React es idéntico: en onClick guardas la función (la campana, lista para sonar), no el resultado de haberla ejecutado (el sonido ya emitido). Guardar la función es lo que permite que React la dispare más tarde, cuando llegue el clic. Esta distinción es tan importante que la lección 3 es entera sobre ella; aquí la sembramos.

Qué es un handler, en concreto

Un event handler no es un tipo especial de cosa de React. Es una función normal de JavaScript, con un único rasgo distintivo: la escribiste para que corra en respuesta a un evento, no ahora mismo. Puede ser una función con nombre declarada dentro del componente, o una función flecha inline. Las dos son válidas; la diferencia es de legibilidad (la lección 7 la trata).

Hay una convención de nombres que conviene seguir desde ya: los handlers se llaman handleAlgohandleClick, handleAddClick, handleChange, handleSubmit—. El prefijo handle marca, de un vistazo, "esta función maneja un evento". Y las props de evento se llaman onAlgoonClick, onChange, onSubmit, y para tus propios callbacks, onAddToCart, onSearch—. La simetría es cómoda: normalmente conectas un handleX a un onX (onClick={handleClick}).

¿Dónde se define un handler? Dentro del componente, casi siempre. La razón es importante y la vas a usar todo el módulo: un handler definido dentro del componente ve las props y el estado de ese render, por el mismo mecanismo de closures de JavaScript. Cuando handleAddClick necesita saber qué producto agregar, lo lee de props.product, que tiene a la mano porque vive dentro de ProductCard. Si lo definieras fuera, no tendría acceso a esas props. Por eso el patrón es: componente afuera, handler adentro, conexión en el JSX.

Ejemplo trabajado: un botón que guarda una función y luego la dispara

Vamos a ver, con código corriendo, las dos mitades del eslabón: primero que en onClick hay una función guardada (no un valor cualquiera, no el resultado de nada), y segundo que "hacer clic" es invocar esa función. Tomemos el ProductCard con un botón "Add to cart". Primero, el componente tal como lo escribirás en React:

function ProductCard(props) {
  function handleAddClick() {
    console.log(`Se agregó "${props.name}"`);
  }
  return (
    <article>
      <h3>{props.name}</h3>
      <button onClick={handleAddClick}>Add to cart</button>
    </article>
  );
}

Léelo con la analogía: handleAddClick es la campana (la acción), el <button> es el pulsador, y onClick={handleAddClick} es el cable. Fíjate en dos cosas. Una: handleAddClick se define dentro de ProductCard, así que ve props.name —por eso puede decir qué producto se agregó—. Dos: en onClick va handleAddClick sin paréntesis —la campana, no el sonido—.

Ahora ejecutémoslo. Usamos la h de siempre, que modela lo que hace React al construir el elemento: guarda onClick como una entrada más en el objeto props. Luego inspeccionamos qué quedó guardado ahí y, por último, "hacemos clic" invocándolo:

function h(tag, props, ...children) {
  return { tag, props: props || {}, children: children.flat() };
}

function ProductCard(props) {
  // el handler: una funcion que se ejecutara CUANDO el usuario haga click
  function handleAddClick() {
    console.log(`  handler corriendo: se agrego "${props.name}"`);
  }
  // onClick guarda la FUNCION (handleAddClick), no la llama (handleAddClick())
  return h('article', {},
    h('h3', {}, props.name),
    h('button', { onClick: handleAddClick }, 'Add to cart')
  );
}

const mouse = { name: 'Wireless Mouse', priceCents: 2599 };
const tree = ProductCard(mouse);
const button = tree.children[1];

console.log('=== Que hay guardado en onClick ===');
console.log('typeof button.props.onClick:', typeof button.props.onClick);
console.log('button.props.onClick.name :', button.props.onClick.name);

console.log('\n=== El usuario hace click: React invoca esa funcion ===');
button.props.onClick(); // esto es lo que hace React cuando ocurre el click

Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:

=== Que hay guardado en onClick ===
typeof button.props.onClick: function
button.props.onClick.name : handleAddClick

=== El usuario hace click: React invoca esa funcion ===
  handler corriendo: se agrego "Wireless Mouse"

Lee las dos partes despacio, porque son las dos mitades del eslabón.

La primera parte confirma qué guardó React (nuestra h) en onClick. typeof button.props.onClick es function: no es un string, ni un número, ni undefined —es una función, esperando para ser llamada—. Y button.props.onClick.name es handleAddClick: es exactamente nuestra función, con su nombre, la que definimos dentro del componente. En ningún momento se ejecutó todavía: construir el elemento (el "render") no la disparó, solo la guardó. La campana está conectada, en silencio.

La segunda parte es el clic. button.props.onClick() —con paréntesis, ahora sí— es lo que hace React cuando el usuario hace clic: busca la función guardada en onClick y la invoca. Y ahí, por fin, corre el handler: imprime se agrego "Wireless Mouse". Fíjate en que el handler supo qué producto era ("Wireless Mouse") porque leyó props.name —lo tenía a la mano por estar definido dentro del componente—.

El eslabón completo, entonces, es: en el render se guarda la función en onClick (sin ejecutarla); en el clic se invoca esa función guardada. Guardar y disparar están separados en el tiempo. Toda la mecánica de eventos de React cabe en esa frase.

Profundización: los eventos que vas a usar, y de dónde salen los handlers

Los eventos más comunes en el storefront. React expone los eventos del DOM como props en camelCase. Los tres que usarás en este módulo:

Prop         Se dispara cuando...                         En Mercado
───────────  ──────────────────────────────────────────  ─────────────────────────────
onClick      el usuario hace click en el elemento         el boton "Add to cart"
onChange     el valor de un input cambia (cada tecla)     la SearchBar mientras escribes
onSubmit     se envia un <form>                           la SearchBar al presionar Enter

Hay muchos más (onMouseEnter, onKeyDown, onFocus, onBlur…), pero la mecánica es idéntica para todos: le pasas una función a la prop, y React la invoca cuando el evento ocurre. Aprende bien estos tres y los demás son variaciones.

Función con nombre vs flecha inline. Las dos formas de escribir el handler son válidas:

// Con nombre (definida dentro del componente):
function handleAddClick() {
  onAddToCart(product);
}
<button onClick={handleAddClick}>Add to cart</button>

// Flecha inline (definida en el propio JSX):
<button onClick={() => onAddToCart(product)}>Add to cart</button>

Las dos conectan una función al clic. La versión con nombre es más legible cuando el handler hace varias cosas; la flecha inline es cómoda cuando es una sola línea o cuando necesitas pasar un argumento (como aquí, product). La lección 7 da el criterio de cuándo usar cada una; por ahora, quédate con que ambas producen lo mismo: una función guardada en onClick.

Por qué el handler se define dentro del componente. Es por el acceso a props y estado. Un handler definido dentro de ProductCard puede leer props.name, props.product, o cualquier estado local, porque JavaScript lo "atrapa" en un closure junto con esas variables. Este es el mecanismo que hace que handleAddClick sepa qué producto agregar sin que se lo digas explícitamente: lo lee de las props que tiene alrededor. Sácalo del componente y pierdes ese acceso. Por eso el patrón universal es: el componente es la función de afuera, el handler es una función de adentro.

Un handler no devuelve nada útil (normalmente). El propósito de un handler es hacer algo —llamar a un setter, avisar al padre—, no devolver un valor. React ignora lo que un handler retorne. Esto conecta con la lección 3: si por error escribes onClick={handleClick()}, lo que guardas en onClick es el valor de retorno del handler, que casi siempre es undefined —inútil—, y encima lo ejecutas en el momento equivocado.

Errores comunes

Definir el handler fuera del componente y perder el acceso a props/estado. Qué pasa: alguien saca handleAddClick a nivel de módulo, fuera de ProductCard, y de pronto el handler ya no sabe qué producto es. Por qué pasa: parece más "ordenado" tener las funciones afuera. Cómo detectarlo: dentro del handler, product o props están undefined o no existen, y tienes que empezar a pasar todo por parámetros a la fuerza. Cómo corregirlo: define el handler dentro del componente, para que vea las props y el estado por closure. Si de verdad necesitas una función externa (por ejemplo, una utilidad pura), pásale los datos como argumentos y llámala desde un handler interno.

Poner texto entre comillas en la prop del evento. Qué pasa: se escribe onClick="handleClick" (con comillas, como en el HTML de toda la vida). Por qué pasa: en HTML plano los atributos de evento se escribían como strings (onclick="doSomething()"). Cómo detectarlo: el clic no hace nada, o React se queja de que onClick recibió un string. Cómo corregirlo: en JSX, el valor de la prop va entre llaves y es una expresión de JavaScript —la función misma—: onClick={handleClick}, no onClick="handleClick". Las comillas te darían el texto "handleClick", no la función.

Confundir la prop del evento (onClick) con el handler (handleClick). Qué pasa: se mezclan los nombres, o se cree que onClick es "la función". Por qué pasa: viven pegados en el JSX y suenan parecido. Cómo detectarlo: te confundes al hablar de "el onClick" cuando quieres decir "el handler". Cómo corregirlo: fija los roles. onClick es la prop del evento —el cable, el punto de conexión que React expone—. handleClick es tu función —la campana, la acción—. onClick={handleClick} conecta la una con la otra. Nombrar bien (onX para la prop, handleX para la función) evita el enredo.

Ejercicios

Ejercicio 1 — ¿Qué hay en onClick? Sin correr nada, di qué imprimirían estas dos líneas, y por qué:

const el = h('button', { onClick: handleAddClick }, 'Add to cart');
console.log(typeof el.props.onClick);
console.log(el.props.onClick.name);

(Asume que handleAddClick es una función con ese nombre, ya definida.)

Ver solución
  • typeof el.props.onClick imprime function. En onClick guardamos handleAddClick sin llamarla, así que ahí quedó la función misma; su tipo es function.
  • el.props.onClick.name imprime handleAddClick. Toda función en JavaScript tiene una propiedad .name con su nombre; como guardamos esa función (no una copia anónima, no su resultado), su nombre es handleAddClick.

La lección: construir el elemento no ejecuta el handler; solo lo guarda. El handler corre después, cuando algo (React, o en el modelo el.props.onClick()) lo invoca. Guardar y disparar están separados.

Ejercicio 2 — Conecta el cable. Este ProductCard define el handler pero no lo conecta. Corrige el JSX para que, al hacer clic en el botón, corra handleAddClick:

function ProductCard(props) {
  function handleAddClick() {
    console.log(`Se agregó "${props.name}"`);
  }
  return (
    <article>
      <h3>{props.name}</h3>
      <button>Add to cart</button>
    </article>
  );
}
Ver solución

Le falta el cable: la prop onClick que conecta el botón con handleAddClick.

function ProductCard(props) {
  function handleAddClick() {
    console.log(`Se agregó "${props.name}"`);
  }
  return (
    <article>
      <h3>{props.name}</h3>
      <button onClick={handleAddClick}>Add to cart</button>
    </article>
  );
}

Se agrega onClick={handleAddClick} al <button>. Fíjate en los tres detalles: llaves (no comillas), el nombre de la función (no un string), y sin paréntesis (pasas la función, no la llamas). Con eso, el clic queda conectado a la acción.

Ejercicio 3 — Nombra bien. Tienes un componente SearchBar que debe reaccionar cuando el usuario escribe. Según las convenciones de la lección, ¿cómo se debería llamar la prop del evento en el <input> y cómo la función handler? Escribe la línea del JSX que las conecta.

Ver solución
  • La prop del evento para "el valor del input cambió" es onChange (convención onX).
  • La función handler se llamaría handleChange (convención handleX).

La línea que las conecta:

<input value={query} onChange={handleChange} />

(O, muy comúnmente, con una flecha inline que lee el evento: onChange={(e) => setQuery(e.target.value)} —eso es la lección 5—.) Lo importante del ejercicio es la simetría de nombres: onChange es el cable que React expone; handleChange es tu función; y se conectan pasando la segunda a la primera, sin paréntesis.

Resumen y siguiente paso

En esta lección clavaste la mecánica base de los eventos. Un event handler es una función normal, definida dentro del componente para que vea sus props y estado, que escribes para que corra en respuesta a un evento. Se conecta al elemento pasándola como prop (onClick={handleAddClick}), y ahí queda guardada —no ejecutada—. Lo mediste: en onClick había una function con nombre (handleAddClick), y construir el elemento no la disparó; solo al invocarla (el "clic") corrió el handler, que además supo qué producto era porque leyó props.name. Guardar y disparar están separados en el tiempo, y en esa separación cabe toda la mecánica de eventos.

Antes de avanzar deberías poder: definir qué es un handler y por qué va dentro del componente; conectar una función a onClick con la sintaxis correcta (llaves, sin comillas, sin paréntesis); distinguir la prop del evento (onClick) de la función handler (handleClick); y nombrar los tres eventos del storefront (onClick, onChange, onSubmit).

La lección 3 toma la distinción que sembramos —guardar la función vs ejecutarla— y la vuelve la protagonista: pasa la función, no la llames. Vas a ver, ejecutado lado a lado, qué pasa cuando escribes onClick={handleClick} (correcto) frente a onClick={handleClick()} (la trampa número uno de React): el segundo corre el handler en el render, en el momento equivocado, y deja el clic conectado a la nada. Ahí, con la salida a la vista, esa trampa deja de poder engañarte.

Recursos