Módulo 4: Events And Handlers
Mantén los handlers limpios
Descripción
Los cuatro temas anteriores del módulo te dieron el qué: conectar eventos, pasar la función, leer el evento, subir datos. Esta lección da el cómo escribirlo bien. Porque un handler puede hacer muchas cosas —frenar el submit, limpiar el texto, validar, avisar al padre— y hay dos maneras de escribirlas: amontonadas dentro del JSX, o sacadas a una función con nombre. La primera vuelve el marcado un nudo ilegible; la segunda deja el JSX limpio y la lógica en un lugar donde se lee y se prueba. Esta lección enseña el criterio, lo aplica al <form onSubmit> de la SearchBar, y ejecuta el handler con nombre como la función normal que es —probándolo directamente, sin navegador—.
Conexión con el módulo. Es la lección de oficio que cierra los temas antes del mini-proyecto. Reúne todo: un handler (L2), pasado sin llamarlo (L3), que usa e.preventDefault() (L4), sobre un formulario que en última instancia sube el dato al padre por un callback (L6). El criterio "saca la lógica del JSX" es el que aplicarás en el proyecto (L8) para que el código del storefront se lea con claridad.
Una analogía: la cocina ordenada vs la encimera atiborrada
Imagina dos cocinas. En la primera, cada vez que preparas un plato, sacas todos los ingredientes, tablas, cuchillos y ollas y los dejas encima de la mesa donde comes, mezclados con los platos servidos. Funciona una vez; pero cuando llegan tres pedidos a la vez, la mesa es un caos y nadie encuentra nada. En la segunda cocina, la preparación ocurre en su propia estación —la barra de trabajo—, con su procedimiento nombrado ("preparar la pasta"), y a la mesa solo llega el plato terminado. La mesa se ve limpia; la receta vive en su estación, donde puedes revisarla, corregirla y reutilizarla.
El JSX es la mesa donde se sirve: debe mostrar, de un vistazo, la estructura de la UI —qué elementos hay y qué eventos disparan—. La lógica del handler es la preparación: validar, normalizar, decidir. Meter la preparación en la mesa —lógica pesada inline en el JSX— atibora el marcado: ya no lees "aquí hay un formulario que al enviarse busca", sino un revoltijo de operaciones. Sacar la lógica a una función con nombre (handleSubmit) es tener la estación de preparación: el JSX queda limpio (onSubmit={handleSubmit}, se lee como una frase), y la lógica vive en su función, donde se lee, se corrige y se prueba. Un handler con nombre es una receta ordenada; un handler inline pesado es la encimera atiborrada.
Ejemplo trabajado: la SearchBar con un handleSubmit con nombre
Vamos a construir la SearchBar como un <form> que, al enviarse, hace varias cosas: frena la recarga, normaliza el texto (quita espacios, pasa a minúsculas), ignora búsquedas vacías y avisa al padre. Es lógica de sobra para justificar sacarla del JSX. Primero, cómo se ve en React de verdad, con el handler con nombre:
function SearchBar({ query, onSearch }) {
function handleSubmit(e) {
e.preventDefault(); // 1) no recargar la pagina
const cleaned = query.trim().toLowerCase(); // 2) normalizar
if (cleaned === '') return; // 3) ignorar busquedas vacias
onSearch(cleaned); // 4) avisar al padre
}
return (
<form onSubmit={handleSubmit}>
<input value={query} /* onChange lo maneja el padre; ver L5/L8 */ />
<button type="submit">Search</button>
</form>
);
}
Mira el JSX: <form onSubmit={handleSubmit}> se lee como una frase —"al enviar este formulario, maneja con handleSubmit"—. Toda la lógica (los cuatro pasos) vive arriba, en la función, ordenada y numerada. Compáralo con la alternativa atiborrada, que mete los cuatro pasos en una flecha inline:
{/* La encimera atiborrada: ilegible y difícil de probar */}
<form onSubmit={(e) => { e.preventDefault(); const c = query.trim().toLowerCase(); if (c === '') return; onSearch(c); }}>
Las dos hacen lo mismo, pero la segunda esconde la estructura de la UI bajo una tira de lógica. La primera es la cocina ordenada.
Ahora ejecutemos el handler con nombre. La gran ventaja de sacarlo del JSX es que es una función normal que podemos llamar directamente para probarla, sin navegador. Le damos dos casos: una búsqueda con espacios y mayúsculas (que debe normalizarse y avisar al padre) y una búsqueda vacía (que debe ignorarse). Modelamos el evento de submit como en la lección 4:
function h(tag, props, ...children) {
return { tag, props: props || {}, children: children.flat() };
}
// Un handler con nombre: una funcion normal. Se lee, se prueba, se reutiliza.
// Contiene la logica de envio: preventDefault, limpiar el texto y avisar al padre.
function makeSearchForm(query, onSearch) {
function handleSubmit(e) {
e.preventDefault(); // 1) no recargar la pagina
const cleaned = query.trim().toLowerCase(); // 2) normalizar
if (cleaned === '') return; // 3) ignorar busquedas vacias
onSearch(cleaned); // 4) avisar al padre (dato hacia arriba)
}
return h('form', { onSubmit: handleSubmit },
h('input', { value: query }),
h('button', { type: 'submit' }, 'Search')
);
}
function onSearch(term) {
console.log(` [App] buscando: "${term}"`);
}
function makeSubmitEvent() {
return { defaultPrevented: false, preventDefault() { this.defaultPrevented = true; } };
}
console.log('=== Un handler con nombre, probado directamente ===\n');
// Caso 1: query con espacios y mayusculas
let form = makeSearchForm(' Mouse ', onSearch);
const e1 = makeSubmitEvent();
console.log('submit con " Mouse ":');
form.props.onSubmit(e1);
console.log(' defaultPrevented =', e1.defaultPrevented);
// Caso 2: query vacia -> no se avisa al padre
form = makeSearchForm(' ', onSearch);
const e2 = makeSubmitEvent();
console.log('submit con " " (vacia):');
form.props.onSubmit(e2);
console.log(' defaultPrevented =', e2.defaultPrevented, '(no se llamo a onSearch)');
Qué esperar. Al correr el archivo con Node, la salida es exactamente esta:
=== Un handler con nombre, probado directamente ===
submit con " Mouse ":
[App] buscando: "mouse"
defaultPrevented = true
submit con " " (vacia):
defaultPrevented = true (no se llamo a onSearch)
Lee los dos casos.
En el caso 1 (" Mouse ", con espacios y mayúsculas), el handleSubmit corrió sus cuatro pasos: frenó la recarga (por eso defaultPrevented = true), normalizó el texto (" Mouse " → "mouse", sin espacios y en minúsculas), no estaba vacío, y avisó al padre —[App] buscando: "mouse"—. El dato subió limpio.
En el caso 2 (" ", solo espacios), mira la diferencia: handleSubmit igual frenó la recarga (defaultPrevented = true), pero al normalizar el texto quedó vacío (" ".trim() es ""), y el paso 3 (if (cleaned === '') return;) cortó la ejecución antes de avisar al padre. Por eso no se llamo a onSearch: no tiene sentido buscar una cadena vacía. La validación funcionó.
Ahora el punto de la lección: pudimos probar todo esto llamando a form.props.onSubmit(e) directamente, con datos fijos, sin navegador ni clics reales —porque handleSubmit es una función normal, no una tira de código atrapada en el JSX—. Un handler con nombre es código que se lee, se corrige y se prueba. Esa es la ventaja concreta de la cocina ordenada.
Profundización: el criterio de cuándo sacar la lógica
La regla general. En el JSX, deja solo la conexión (onSubmit={handleSubmit}, onClick={handleAddClick}); saca la lógica a una función con nombre dentro del componente. El JSX debe leerse como una descripción de la UI, no como un programa. Si al mirar tu JSX no puedes decir de un vistazo "aquí hay un formulario que al enviarse busca", la lógica está estorbando y hay que sacarla.
Cuándo una flecha inline SÍ está bien. No toda lógica inline es mala. Una flecha corta y de una sola responsabilidad es perfectamente legible, y a veces necesaria:
// Bien: una linea, y necesita pasar un argumento
<button onClick={() => onAddToCart(product)}>Add to cart</button>
// Bien: una linea, actualizar el estado del input
<input value={query} onChange={(e) => setQuery(e.target.value)} />
Estas flechas no esconden nada: se leen de un vistazo, y su razón de existir es legítima —pasar un argumento (product) o leer el evento (e.target.value)—. El problema no es "inline"; el problema es inline y pesado: varios pasos, validaciones, condiciones. La línea divisoria es la legibilidad: si la flecha cabe cómoda en una línea y hace una cosa, déjala; si crece o ramifica, dale un nombre.
Dónde va la función con nombre. Dentro del componente, arriba del return (como handleSubmit en el ejemplo). Ahí ve las props y el estado por closure (lección 2), y queda junto al JSX que la usa. No la saques del componente salvo que sea lógica pura reutilizable sin props/estado.
Un handler, una responsabilidad de UI. Un handleSubmit puede orquestar varios pasos (frenar, normalizar, validar, avisar), pero todos sirven a un evento (el envío). Si te descubres metiendo en un mismo handler cosas de eventos distintos, sepáralos en handlers distintos (handleSubmit, handleClear, handleChange). Un evento, un handler.
El beneficio de fondo: código probable. El mayor argumento para sacar la lógica del JSX no es estético, es que la vuelve verificable. En el ejemplo, probamos handleSubmit con dos entradas y confirmamos su comportamiento sin montar la UI. Un handler enterrado en el JSX no se puede llamar por su cuenta; uno con nombre, sí. En esta guía, además, es lo que nos deja ejecutar la lógica en Node —el hilo de toda la guía—: la lógica limpia y con nombre es la lógica que podemos correr y medir.
Errores comunes
Amontonar lógica pesada en una flecha inline. Qué pasa: el onSubmit (o el onClick) es una flecha de varias sentencias con validaciones y condiciones, y el JSX se vuelve ilegible. Por qué pasa: se escribe "sobre la marcha", agregando pasos a la flecha sin parar a extraerla. Cómo detectarlo: tu prop de evento tiene un { ... } con ; adentro, o no cabe cómoda en una línea. Cómo corregirlo: saca la lógica a una función con nombre (handleSubmit) arriba del return, y deja en el JSX solo onSubmit={handleSubmit}. La UI se lee, y la lógica queda probable.
Sacar a función con nombre pero luego llamarla en el JSX. Qué pasa: se extrae bien el handleSubmit, pero al conectarlo se escribe onSubmit={handleSubmit()} con paréntesis. Por qué pasa: se mezcla la buena práctica de nombrar con el error de la lección 3. Cómo detectarlo: el handler corre en el render en vez de en el evento (el bug clásico). Cómo corregirlo: conéctalo sin paréntesis: onSubmit={handleSubmit}. Nombrar bien no te exime de pasar la función sin llamarla.
Extraer de más (una función con nombre para cada flecha trivial). Qué pasa: se crean handlers con nombre para cosas de una línea que ya eran legibles inline, y el componente se llena de funciones diminutas. Por qué pasa: se aplica "saca todo" como regla ciega. Cómo detectarlo: tienes un handleAddClick que solo hace onAddToCart(product) y nada más, repetido por todos lados. Cómo corregirlo: usa el criterio, no el reflejo. Si la flecha es una línea y de una responsabilidad (() => onAddToCart(product)), déjala inline; extrae cuando la lógica crece. Ni encimera atiborrada, ni mil estaciones para una sola tarea.
Ejercicios
Ejercicio 1 — Ordena la cocina. Este onSubmit tiene la lógica amontonada inline. Sácala a una función con nombre handleSubmit dentro del componente, dejando el JSX limpio:
function SearchBar({ query, onSearch }) {
return (
<form onSubmit={(e) => { e.preventDefault(); const c = query.trim(); if (c === '') return; onSearch(c); }}>
<input value={query} />
<button type="submit">Search</button>
</form>
);
}
Ver solución
function SearchBar({ query, onSearch }) {
function handleSubmit(e) {
e.preventDefault();
const cleaned = query.trim();
if (cleaned === '') return;
onSearch(cleaned);
}
return (
<form onSubmit={handleSubmit}>
<input value={query} />
<button type="submit">Search</button>
</form>
);
}
La lógica (frenar, normalizar, validar, avisar) vive ahora en handleSubmit, arriba del return, donde ve query y onSearch por closure. El JSX queda limpio: <form onSubmit={handleSubmit}> se lee como una frase. Y handleSubmit es una función normal que podrías probar directamente. Fíjate en que se conecta sin paréntesis: onSubmit={handleSubmit}.
Ejercicio 2 — ¿Inline o con nombre? Para cada handler, di si lo dejarías inline o lo sacarías a una función con nombre, y por qué: (a) onChange={(e) => setQuery(e.target.value)}; (b) un onSubmit que valida tres campos, arma un objeto y llama a una API; (c) onClick={() => onAddToCart(product)}; (d) un onClick que, según el rol del usuario, hace tres cosas distintas con condiciones.
Ver solución
- (a) Inline. Una línea, una responsabilidad (actualizar el estado con el valor tecleado), y necesita el
e. Se lee de un vistazo. Déjala inline. - (b) Con nombre. Validar tres campos, armar un objeto y llamar a una API es lógica pesada de varios pasos; inline volvería el JSX ilegible y no se podría probar por su cuenta. Sácala a
handleSubmit. - (c) Inline. Una línea, y la flecha es necesaria para pasar el argumento
productsin llamar en el render. Legible. Déjala inline. - (d) Con nombre. Ramifica según el rol y hace varias cosas: es lógica que merece su función
handleClick(o varias), tanto por legibilidad como para poder probar cada rama.
El criterio, en una frase: una línea y una responsabilidad → inline; varios pasos o ramas → con nombre.
Ejercicio 3 — Prueba el handler. Usando el modelo del ejemplo trabajado, ¿qué imprime este código? Explica por qué la búsqueda " KEYBOARD " termina como termina.
const form = makeSearchForm(' KEYBOARD ', onSearch);
const e = makeSubmitEvent();
form.props.onSubmit(e);
console.log('defaultPrevented =', e.defaultPrevented);
Ver solución
Imprime:
[App] buscando: "keyboard"
defaultPrevented = true
Paso a paso dentro de handleSubmit:
e.preventDefault()levanta la bandera →defaultPreventedpasa atrue(frena la recarga).const cleaned = query.trim().toLowerCase():" KEYBOARD "→.trim()quita los espacios →"KEYBOARD"→.toLowerCase()→"keyboard".cleanedno está vacío, así que no entra elreturn.onSearch("keyboard")avisa al padre, que imprime[App] buscando: "keyboard".
Al final, defaultPrevented = true. La búsqueda terminó como "keyboard" porque la normalización (paso 2) le quitó los espacios y la pasó a minúsculas antes de subirla al padre. Y todo esto lo probamos llamando al handler directamente, sin navegador: la ventaja de tenerlo con nombre.
Resumen y siguiente paso
En esta lección aprendiste el oficio de escribir handlers legibles. La regla: en el JSX, solo la conexión; la lógica, en una función con nombre —la cocina ordenada, no la encimera atiborrada—. Un handleSubmit con nombre reúne sus pasos (frenar con preventDefault, normalizar, validar, avisar al padre) en un lugar donde se lee, se corrige y —clave para esta guía— se prueba: lo llamaste directamente con dos entradas y confirmaste que normaliza " Mouse " → "mouse" y que ignora una búsqueda vacía, sin navegador. Y viste el matiz: una flecha inline corta y de una responsabilidad (() => onAddToCart(product), (e) => setQuery(e.target.value)) está bien; lo que estorba es la lógica pesada inline. El criterio: una línea y una cosa → inline; varios pasos o ramas → con nombre.
Antes de avanzar deberías poder: decidir con criterio si un handler va inline o con nombre; sacar lógica pesada del JSX a una función con nombre dentro del componente; explicar por qué un handler con nombre se puede probar y uno enterrado en el JSX no; y conectar el handler con nombre sin llamarlo (onSubmit={handleSubmit}).
La lección 8 es el mini-proyecto: cablear los eventos del storefront. Vas a juntar los cuatro pilares del módulo —conectar eventos, pasar la función, el input controlado, subir datos por callbacks— y los handlers limpios de esta lección, para darle interactividad completa al storefront: una SearchBar controlada que sube el texto al App, y ProductCard con "Add to cart" que sube el producto al App, que llena el carrito de forma inmutable. Todo ejecutado en Node en una sesión de usuario completa.
Recursos
- React, "Responding to Events" — react.dev/learn/responding-to-events. Muestra handlers con nombre definidos dentro del componente y conectados en el JSX; el estilo que esta lección promueve. En inglés.
- React, "Keeping Components Pure" — react.dev/learn/keeping-components-pure. Por qué el render debe describir y la lógica de eventos vivir en handlers; el fondo de "no metas lógica pesada en el JSX". En inglés.
- React, "You Might Not Need an Effect" — react.dev/learn/you-might-not-need-an-effect. Dónde vive cada tipo de lógica (eventos vs efectos vs render); útil para no cargar el JSX ni los efectos con lógica que va en un handler. En inglés.
- MDN, "Arrow function expressions" — developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions. La sintaxis de la flecha inline y cuándo una expresión corta es más clara que una función con nombre. En inglés.