Módulo 4: Events And Handlers
Pasa la función, no la llames
Descripción
Hay una sola línea de código que atrapa a casi todo el que empieza con React, y es tan pequeña que parece un detalle sin importancia: la diferencia entre onClick={handleClick} y onClick={handleClick()}. Un par de paréntesis. Pero esos paréntesis cambian por completo cuándo corre tu handler, y con el cambio se rompe la interacción. Esta lección toma esa trampa, la pone bajo la lupa, y la demuestra ejecutando —para que la veas fallar con tus propios ojos y nunca más te confunda—.
Conexión con el módulo. Es la regla dura de React sobre eventos, y aplica a todas las props de evento, no solo a onClick: onChange, onSubmit, y tus propios callbacks (onAddToCart) siguen la misma regla. Se apoya en la lección 2 (un handler es una función que se guarda en la prop y se dispara después) y prepara todo lo que viene: en la lección 4 verás handlers que reciben el evento, en la 5 el onChange de la SearchBar, en la 6 el onAddToCart con su caso legítimo de "envolver en una flecha". Si esta regla queda firme, esos casos se leen solos.
Una analogía: el timbre, otra vez, pero con el error
Retomemos el timbre. Instalar el timbre es conectar el cable entre el botón y la campana, para que la campana suene cuando alguien presione. Ahora imagina que, al instalarlo, en vez de conectar el mecanismo de la campana, haces sonar la campana una vez —din-don— y crees que con eso quedó "conectada". ¿Qué pasó? Dos cosas malas a la vez. Primera: la campana sonó en el momento equivocado —mientras instalabas, sin que nadie hubiera llegado a la puerta—. Segunda: al cable no le quedó conectado ningún mecanismo, así que cuando por fin llegue una visita y presione el botón, no sonará nada. Sonó cuando no debía y no suena cuando debe.
Eso es, exactamente, escribir onClick={handleClick()}. Los paréntesis () significan "ejecuta la función ahora". Así que handleClick() corre durante el render —el momento en que React construye el elemento, el equivalente a instalar el timbre—, en el momento equivocado. Y lo que queda guardado en onClick no es la función, sino lo que la función devolvió —el "din-don ya sonado", que normalmente es undefined—. Cuando el usuario haga clic de verdad, React buscará en onClick algo que invocar y encontrará undefined: no suena nada.
En cambio, onClick={handleClick} —sin paréntesis— conecta el mecanismo: guarda la función, lista para sonar, y React la dispara cuando llegue el clic. Suena cuando debe, y no antes. La regla, en una frase: a la prop del evento le pasas la campana (la función), no el sonido (el resultado de llamarla).
Ejemplo trabajado: los dos casos, lado a lado
Vamos a correr los dos casos —el bueno y el malo— y a mirar dos cosas en cada uno: cuándo corre el handler y qué queda guardado en onClick. Usamos un handleClick que imprime cuando corre y devuelve undefined (como casi todos los handlers). Y separamos con claridad dos momentos: construir el elemento (que modela el render) y hacer clic (invocar el onClick).
function h(tag, props, ...children) {
return { tag, props: props || {}, children: children.flat() };
}
function handleClick() {
console.log(' >> handleClick corrio!');
return undefined;
}
console.log('=== CASO A: onClick: handleClick (pasar la funcion) ===');
console.log('Construyendo el elemento (esto modela el render)...');
const good = h('button', { onClick: handleClick }, 'Add');
console.log(' (durante el render NO se imprimio nada: bien)');
console.log('typeof good.props.onClick:', typeof good.props.onClick);
console.log('Ahora el usuario hace click:');
if (typeof good.props.onClick === 'function') good.props.onClick();
console.log('\n=== CASO B: onClick: handleClick() (LLAMAR la funcion) ===');
console.log('Construyendo el elemento (esto modela el render)...');
const bad = h('button', { onClick: handleClick() }, 'Add');
console.log('typeof bad.props.onClick:', typeof bad.props.onClick);
console.log('Ahora el usuario hace click:');
if (typeof bad.props.onClick === 'function') bad.props.onClick();
else console.log(' (nada que invocar: onClick vale ' + bad.props.onClick + ')');
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== CASO A: onClick: handleClick (pasar la funcion) ===
Construyendo el elemento (esto modela el render)...
(durante el render NO se imprimio nada: bien)
typeof good.props.onClick: function
Ahora el usuario hace click:
>> handleClick corrio!
=== CASO B: onClick: handleClick() (LLAMAR la funcion) ===
Construyendo el elemento (esto modela el render)...
>> handleClick corrio!
typeof bad.props.onClick: undefined
Ahora el usuario hace click:
(nada que invocar: onClick vale undefined)
Lee los dos casos en paralelo, porque el contraste es la lección entera.
En el Caso A (onClick: handleClick, sin paréntesis), mira el orden. Construimos el elemento y —fíjate— durante el render no se imprimió nada: handleClick no corrió, solo se guardó. Lo confirma la línea siguiente: typeof good.props.onClick es function —ahí está la función, esperando—. Y solo cuando "el usuario hace clic" (good.props.onClick()) aparece >> handleClick corrio!. La campana sonó cuando debía: en el clic, no antes.
En el Caso B (onClick: handleClick(), con paréntesis), todo se corre de lugar. Mira: >> handleClick corrio! aparece durante la construcción del elemento —en el render, el momento equivocado—, porque los paréntesis lo ejecutaron ahí mismo. Lo que quedó en onClick es el valor de retorno de handleClick, que es undefined: por eso typeof bad.props.onClick es undefined, no function. Y cuando "el usuario hace clic", no hay nada que invocar —onClick vale undefined—: la rama del else lo dice. La campana sonó al instalar, y en el clic real hay silencio.
Ese es el doble desastre de los paréntesis, medido: el handler corre en el render (a veces con efectos visibles y hasta bucles infinitos si dentro llama a un setter), y el clic queda muerto. Un solo par de paréntesis, dos bugs.
Profundización: por qué pasa, y el caso legítimo de "llamar"
Por qué es tan fácil equivocarse. En JavaScript de todos los días, para usar una función la llamas: formatPrice(2599), products.filter(...). Los paréntesis son el modo normal de "hacer que la función haga su trabajo". Pero en onClick={...} el trabajo de la función es para después —cuando ocurra el clic—, no para ahora. Lo que necesitas ahí no es usar la función, sino entregársela a React para que él la use en el momento correcto. Entregar una función es escribir su nombre sin paréntesis: handleClick. Es la misma diferencia que entre dar una receta y cocinar el plato: en onClick das la receta (la función); React cocina el plato (la llama) cuando llegue el evento.
Un truco mental infalible. Pregúntate: "¿quiero que esto corra ahora, mientras se pinta el componente, o después, cuando el usuario actúe?". Si la respuesta es "después" —y para un handler siempre es "después"—, entonces no lleva paréntesis. Los paréntesis significan "ahora".
El caso legítimo de "llamar": envolver en una flecha. Hay una situación real en la que necesitas pasarle un argumento al handler —por ejemplo, onAddToCart(product), donde el ProductCard quiere avisar qué producto—. No puedes escribir onClick={onAddToCart(product)}: eso llamaría a onAddToCart en el render (el error que acabamos de ver). La solución es envolver la llamada en una función flecha:
<button onClick={() => onAddToCart(product)}>Add to cart</button>
Ahora lo que le pasas a onClick es una función —la flecha () => onAddToCart(product)—, no el resultado de nada. La flecha se guarda, esperando; y solo cuando el usuario hace clic, React la invoca, y entonces la flecha llama a onAddToCart(product) con el argumento. Volviendo a la analogía: la flecha es una campana nueva cuyo único trabajo es "sonar la campana grande con este dato". Guardas la campana nueva (bien), no la haces sonar al instalar. Verifiquémoslo ejecutando:
function h(tag, props, ...children) {
return { tag, props: props || {}, children: children.flat() };
}
function addToCart(product) {
console.log(` agregado al carrito: ${product.name}`);
}
const mouse = { name: 'Wireless Mouse' };
// MAL: addToCart(mouse) corre en el render y onClick queda en undefined.
// BIEN: envuelves en una flecha; la flecha ES la funcion, y solo se llama en el click.
const button = h('button', { onClick: () => addToCart(mouse) }, 'Add');
console.log('typeof onClick:', typeof button.props.onClick);
console.log('(sin salida todavia: la flecha no se ha invocado)');
console.log('click ->');
button.props.onClick();
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
typeof onClick: function
(sin salida todavia: la flecha no se ha invocado)
click ->
agregado al carrito: Wireless Mouse
Léelo: typeof onClick es function —la flecha se guardó, no se ejecutó—, y por eso "sin salida todavía". El addToCart(mouse) de adentro no corrió en el render: la flecha lo tiene envuelto, en pausa. Solo al "hacer clic" (button.props.onClick()) la flecha se invoca y ahí llama a addToCart(mouse), que imprime agregado al carrito: Wireless Mouse. Ese es el patrón correcto para pasar argumentos, y lo usarás en la lección 6 con onAddToCart(product).
Resumen de las tres formas:
Escribes onClick guarda... Corre el handler...
──────────────────────────────────── ─────────────────────── ────────────────────────
onClick={handleClick} la funcion handleClick en el click (BIEN)
onClick={handleClick()} undefined (el retorno) en el render (MAL)
onClick={() => addToCart(product)} la flecha en el click (BIEN, con arg)
Errores comunes
onClick={handleClick()} — llamar en vez de pasar. Qué pasa: el handler corre solo, apenas se pinta el componente, y el clic real no hace nada. Por qué pasa: la costumbre de JavaScript de llamar las funciones con paréntesis. Cómo detectarlo: ves el efecto del handler (un log, un cambio) sin haber hecho clic, justo cuando el componente aparece; y hacer clic no reacciona. Si el handler llama a un setter, puede ser peor: el setter dispara un re-render, que vuelve a ejecutar el handler, que vuelve a llamar al setter… un bucle infinito con "Too many re-renders". Cómo corregirlo: quita los paréntesis —onClick={handleClick}—. Si necesitabas pasar un argumento, envuélvelo en una flecha —onClick={() => handleClick(arg)}—.
Envolver en flecha cuando no hacía falta. Qué pasa: se escribe onClick={() => handleClick()} cuando handleClick no lleva argumentos. Por qué pasa: se aplica el patrón de la flecha por reflejo, "por si acaso". Cómo detectarlo: la flecha solo llama a la función sin pasarle nada. Cómo corregirlo: no está mal (funciona igual), pero es ruido: si no pasas argumentos, onClick={handleClick} es más directo. Reserva la flecha para cuando de verdad necesitas pasar un dato (() => onAddToCart(product)) o poner varias líneas.
Creer que quitar los paréntesis "no ejecuta el handler nunca". Qué pasa: después de aprender la regla, alguien duda: "si no le pongo paréntesis, ¿entonces cómo se llega a ejecutar?". Por qué pasa: se confunde quién llama la función. Cómo detectarlo: la pregunta misma. Cómo corregirlo: recuerda que la llamas tú al conectar, o la llama React al ocurrir el evento. Al escribir onClick={handleClick} no la llamas tú —bien, no debe correr todavía—, pero React la llamará por ti cuando el usuario haga clic. No tienes que ponerle paréntesis: React se los pone en el momento correcto. Tú solo entregas la función.
Ejercicios
Ejercicio 1 — ¿Cuándo corre? Para cada línea, di si el handler corre en el render, en el clic, o nunca, y qué queda guardado en onClick:
(a) <button onClick={handleAddClick}>Add</button>
(b) <button onClick={handleAddClick()}>Add</button>
(c) <button onClick={() => handleAddClick()}>Add</button>
Ver solución
- (a) Corre en el clic. En
onClickse guarda la funciónhandleAddClick(sin llamarla). React la invoca cuando el usuario hace clic. Correcto. - (b) Corre en el render. Los paréntesis ejecutan
handleAddClickal construir el elemento; enonClickqueda su valor de retorno (normalmenteundefined). En el clic no pasa nada. Incorrecto (el bug clásico). - (c) Corre en el clic. En
onClickse guarda la flecha() => handleAddClick(); es una función, se guarda sin ejecutarse. Cuando el usuario hace clic, React invoca la flecha, y esta llama ahandleAddClick(). Correcto, aunque la flecha aquí es innecesaria (no pasa argumentos):onClick={handleAddClick}bastaría.
Ejercicio 2 — Pasar un argumento. Tienes function addToCart(product) {...} y quieres que al hacer clic se agregue este product. Escribe el <button> correcto y explica por qué onClick={addToCart(product)} no sirve.
Ver solución
El botón correcto envuelve la llamada en una flecha:
<button onClick={() => addToCart(product)}>Add to cart</button>
Así, en onClick se guarda la flecha (una función), que solo se ejecuta en el clic, y entonces llama a addToCart(product) con el argumento.
onClick={addToCart(product)} no sirve porque los paréntesis ejecutan addToCart(product) durante el render, en el momento equivocado; el producto se "agregaría" apenas se pinta la tarjeta, sin que nadie haya hecho clic. Y en onClick quedaría el valor de retorno de addToCart (probablemente undefined), así que el clic real no haría nada. Necesitas la flecha para diferir la llamada hasta el clic.
Ejercicio 3 — El bucle infinito. Alguien escribe <button onClick={setCount(count + 1)}>+1</button> y la app se cuelga con el error "Too many re-renders". Explica, paso a paso, por qué, y da el arreglo.
Ver solución
El problema son los paréntesis: setCount(count + 1) se ejecuta durante el render, no en el clic. Y ahí se dispara un ciclo:
- React renderiza el componente y, al construir el
<button>, ejecutasetCount(count + 1)(por los paréntesis). setCountcambia el estado, lo que dispara un nuevo render.- En ese nuevo render, React vuelve a construir el
<button>y vuelve a ejecutarsetCount(count + 1). - Que dispara otro render… y así infinitamente. React corta el ciclo y lanza "Too many re-renders".
El handler nunca esperó al clic: corría en cada render, y cada corrida provocaba el siguiente render.
El arreglo es pasar la función sin llamarla, envolviendo en una flecha porque hay que expresar "sumar uno":
<button onClick={() => setCount(count + 1)}>+1</button>
O, mejor aún con el updater funcional del módulo 3:
<button onClick={() => setCount((c) => c + 1)}>+1</button>
Ahora setCount solo corre cuando el usuario hace clic, un render por clic, sin bucle.
Resumen y siguiente paso
En esta lección clavaste la regla dura de los eventos en React: pasa la función, no la llames. onClick={handleClick} conecta la función (se guarda, corre en el clic); onClick={handleClick()} la ejecuta en el render —el momento equivocado— y deja en onClick su valor de retorno (undefined), así que el clic queda muerto. Lo mediste lado a lado: el caso bueno no imprimió nada al construir el elemento y sí al hacer clic; el caso malo imprimió al construir y nada al hacer clic. Y viste el único caso legítimo de "llamar": envolver en una flecha cuando necesitas pasar un argumento —onClick={() => addToCart(product)}—, que guarda la flecha (una función) y difiere la llamada real hasta el clic. La regla aplica a toda prop de evento: onChange, onSubmit, onAddToCart.
Antes de avanzar deberías poder: explicar qué hace de más y qué de menos onClick={handleClick()}; decir cuándo corre el handler en cada una de las tres formas (fn, fn(), () => fn(arg)); y escribir correctamente un handler que necesita pasar un argumento.
La lección 4 abre la caja de lo que React le entrega al handler cuando por fin lo dispara: el objeto evento. Vas a ver, ejecutado, cómo un onChange recibe un evento del que sacas el texto tecleado con e.target.value, y cómo un onSubmit frena la recarga de la página con e.preventDefault(). Con la regla de esta lección firme (pasar la función), ahora entramos a qué recibe esa función.
Recursos
- React, "Responding to Events: Passing event handlers" — react.dev/learn/responding-to-events#passing-event-handlers-as-props. La doc oficial sobre pasar handlers sin llamarlos y el patrón de la flecha para argumentos; la referencia directa de esta lección. En inglés.
- React, "Responding to Events" — react.dev/learn/responding-to-events. La página completa: incluye la advertencia explícita sobre
onClick={handleClick()}vsonClick={handleClick}. En inglés. - MDN, "Functions" — developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions. La diferencia, en JavaScript puro, entre una función (
fn), su llamada (fn()) y su valor de retorno; el fondo del error de esta lección. En inglés. - MDN, "Arrow function expressions" — developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions. Qué es una función flecha, la herramienta con la que envuelves una llamada para diferirla hasta el evento. En inglés.