Manejo de eventos
Responde a las interacciones del usuario con manejadores de eventos con seguridad de tipos.
Receta
Tarjeta de referencia rápida — lista para copiar y pegar.
// Manejador en línea
<button onClick={() => console.log("clicked")}>Haz clic aquí</button>
// Manejador con nombre
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
console.log(e.currentTarget.name);
}
<button name="save" onClick={handleClick}>Guardar</button>
// Pasar datos a los manejadores
<button onClick={() => deleteItem(item.id)}>Eliminar</button>
// Prevenir el comportamiento por defecto
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>Cuándo recurrir a esto: Siempre que necesites responder a clics, pulsaciones de teclas, envíos de formularios, cambios de foco o cualquier otro evento del DOM.
Ejemplo funcional
"use client";
import { useState, useCallback } from "react";
interface LogEntry {
id: number;
type: string;
detail: string;
timestamp: string;
}
let entryId = 0;
export function EventPlayground() {
const [log, setLog] = useState<LogEntry[]>([]);
const addLog = useCallback((type: string, detail: string) => {
setLog(prev => [
{ id: ++entryId, type, detail, timestamp: new Date().toLocaleTimeString() },
...prev.slice(0, 9), // conservar los últimos 10
]);
}, []);
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
addLog("click", `Botón "${e.currentTarget.textContent}" en (${e.clientX}, ${e.clientY})`);
}
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === "Enter") {
addLog("keydown", `Enter pulsado — valor: "${e.currentTarget.value}"`);
e.currentTarget.value = "";
}
}
function handleFocus(e: React.FocusEvent<HTMLInputElement>) {
addLog("focus", `Entrada "${e.currentTarget.placeholder}" enfocada`);
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
addLog("submit", `Formulario enviado con nombre: "${formData.get("name")}"`);
e.currentTarget.reset();
}
return (
<div className="max-w-md space-y-4 rounded border p-4">
<div className="flex gap-2">
<button onClick={handleClick} className="rounded bg-blue-600 px-3 py-1 text-white">
Botón A
</button>
<button onClick={handleClick} className="rounded bg-green-600 px-3 py-1 text-white">
Botón B
</button>
</div>
<input
onKeyDown={handleKeyDown}
onFocus={handleFocus}
placeholder="Pulsa Enter aquí..."
className="w-full rounded border px-3 py-1"
/>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
name="name"
placeholder="Tu nombre"
onFocus={handleFocus}
className="flex-1 rounded border px-3 py-1"
/>
<button type="submit" className="rounded bg-purple-600 px-3 py-1 text-white">
Enviar
</button>
</form>
<div className="max-h-48 overflow-y-auto rounded bg-gray-50 p-2">
<h3 className="mb-1 text-xs font-bold uppercase text-gray-500">Registro de eventos</h3>
{log.length === 0 && <p className="text-xs text-gray-400">Interactúa con los controles de arriba...</p>}
{log.map(entry => (
<div key={entry.id} className="border-b py-1 text-xs">
<span className="font-mono font-bold text-blue-700">{entry.type}</span>{" "}
<span className="text-gray-600">{entry.detail}</span>{" "}
<span className="text-gray-400">{entry.timestamp}</span>
</div>
))}
</div>
</div>
);
}Qué demuestra esto:
- Múltiples tipos de eventos:
onClick,onKeyDown,onFocus,onSubmit - Objetos de evento tipados con
React.MouseEvent,React.KeyboardEvent,React.FocusEvent,React.FormEvent e.preventDefault()para evitar que el envío del formulario recargue la páginae.currentTargetpara acceder al elemento al que está asociado el manejadoruseCallbackpara estabilizar una función auxiliar que se llama con frecuencia
Análisis en profundidad
Cómo funciona
- React asocia un único listener de eventos en la raíz del árbol (delegación de eventos), no en cada elemento individual
- Cuando se dispara un evento del DOM, React envuelve el evento nativo en un
SyntheticEventque normaliza las diferencias entre navegadores e.currentTargetes el elemento al que está asociado el manejador;e.targetes el elemento que originó el evento (puede ser un hijo)- Los eventos de React se propagan (bubble) a través del árbol de React, que puede diferir del árbol del DOM cuando se usan portales
- Los manejadores de eventos se ejecutan durante el despacho de eventos del navegador — son síncronos y tienen acceso al objeto del evento
Tipos de eventos comunes
| Prop de React | Tipo de TypeScript | Evento nativo |
|---|---|---|
onClick | React.MouseEvent<HTMLElement> | click |
onChange | React.ChangeEvent<HTMLInputElement> | input (React se dispara en cada pulsación de tecla) |
onSubmit | React.FormEvent<HTMLFormElement> | submit |
onKeyDown | React.KeyboardEvent<HTMLElement> | keydown |
onFocus | React.FocusEvent<HTMLElement> | focus |
onBlur | React.FocusEvent<HTMLElement> | blur |
onMouseEnter | React.MouseEvent<HTMLElement> | mouseenter |
onPointerDown | React.PointerEvent<HTMLElement> | pointerdown |
onDrag | React.DragEvent<HTMLElement> | drag |
Variaciones
Detener la propagación:
function handleInner(e: React.MouseEvent) {
e.stopPropagation(); // evita que se dispare el onClick exterior
}
<div onClick={() => console.log("outer")}>
<button onClick={handleInner}>Haz clic aquí</button>
</div>Fase de captura:
// La variante "Capture" se dispara de arriba hacia abajo antes de la fase de propagación
<div onClickCapture={() => console.log("captured first")}>
<button onClick={() => console.log("bubbled second")}>Clic</button>
</div>Acceder al evento nativo:
function handleClick(e: React.MouseEvent) {
const nativeEvent = e.nativeEvent; // el Event subyacente del DOM
console.log(nativeEvent instanceof MouseEvent); // true
}Datos personalizados mediante closures:
function handleDelete(itemId: string) {
setItems(prev => prev.filter(i => i.id !== itemId));
}
{items.map(item => (
<button key={item.id} onClick={() => handleDelete(item.id)}>
Eliminar {item.name}
</button>
))}Notas sobre TypeScript
// Tipar una prop de manejador
interface SearchBarProps {
onSearch: (query: string) => void;
}
function SearchBar({ onSearch }: SearchBarProps) {
return (
<input
type="search"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
onSearch(e.target.value);
}}
/>
);
}
// Tipo de manejador en línea (útil para props de callback)
type ClickHandler = React.MouseEventHandler<HTMLButtonElement>;
// Tipo de elemento genérico
function logEvent<T extends HTMLElement>(e: React.SyntheticEvent<T>) {
console.log(e.currentTarget.tagName);
}Errores comunes
-
Llamar a la función en lugar de pasarla —
onClick={handleClick()}invoca la función durante el render, no al hacer clic. Solución: Pasa la referencia:onClick={handleClick}o envuélvela en una función flecha:onClick={() => handleClick(arg)}. -
Closures obsoletos en los manejadores — Un manejador de eventos creado dentro de un componente captura el estado en el momento del render. Si el estado se actualiza antes de que se ejecute el manejador (algo raro), el manejador ve valores antiguos. Solución: Usa
useCallbackcon las dependencias correctas, o lee desde una ref para obtener los valores realmente más recientes. -
onChangede React vschangedel DOM — ElonChangede React se dispara en cada pulsación de tecla (como el eventoinputdel DOM), no al perder el foco como el eventochangenativo. Esto sorprende a los desarrolladores que vienen de JavaScript puro. Solución: Simplemente tenlo en cuenta — es el comportamiento previsto. -
Listeners de eventos pasivos — React no admite la opción
{ passive: true }mediante props de JSX. Para manejadores de touch/wheel donde necesitaspassive: falsepara poder llamar apreventDefault(), puede que necesites unaddEventListenermanual dentro de unuseEffect. Solución: UsauseEffect+refpara asociar el listener nativo. -
Funciones flecha en el render —
onClick={() => doSomething(id)}crea una nueva función en cada render, lo que puede anularReact.memo. Solución: Para rutas críticas (listas grandes), extrae el manejador o usauseCallback. Para la mayoría de los casos está bien — no optimices prematuramente.
Alternativas
| Alternativa | Úsala cuando | No la uses cuando |
|---|---|---|
useEffect + addEventListener | Necesitas listeners pasivos, eventos a nivel de window o eventos que React no envuelve | El evento está en un elemento específico de React (usa un manejador de JSX) |
| Form Actions (React 19) | Manejas envíos de formularios con server actions | Formularios solo de cliente sin interacción con el servidor |
| Delegación de eventos basada en refs | Construyes una librería que necesita control directo de los eventos del DOM | Código de aplicación normal |
Preguntas frecuentes
¿Qué es un SyntheticEvent en React?
Un envoltorio multinavegador sobre el evento nativo del DOM. React crea un SyntheticEvent para cada manejador de eventos, normalizando las diferencias entre navegadores. Accede al evento nativo mediante e.nativeEvent si lo necesitas.
¿Cuál es la diferencia entre e.target y e.currentTarget?
e.currentTarget— el elemento al que está asociado el manejadore.target— el elemento que realmente disparó el evento (puede ser un hijo)- Usa
currentTargetcuando leas atributos del elemento al que asociaste el manejador
¿Por qué onClick={handleClick()} llama a la función de inmediato?
Añadir () invoca la función durante el render y pasa su valor de retorno como manejador. En su lugar, pasa la referencia: onClick={handleClick} o envuélvela: onClick={() => handleClick(arg)}.
¿Cómo paso datos a un manejador de eventos?
Envuelve el manejador en una función flecha:
<button onClick={() => deleteItem(item.id)}>Eliminar</button>¿Cómo evito que un formulario recargue la página al enviarlo?
Llama a e.preventDefault() en el manejador onSubmit:
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>¿Cómo tipo un manejador de eventos en TypeScript?
Usa los tipos de eventos de React con el genérico del elemento HTML:
function handleClick(e: React.MouseEvent<HTMLButtonElement>) { ... }
function handleChange(e: React.ChangeEvent<HTMLInputElement>) { ... }
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }¿onChange de React se dispara en cada pulsación de tecla?
Sí. El onChange de React se comporta como el evento input nativo, disparándose en cada pulsación de tecla — no al perder el foco como el evento change nativo. Esto es intencional para actualizaciones de formularios en tiempo real.
¿Cómo evito que un evento se propague a los manejadores padres?
Llama a e.stopPropagation() en el manejador de eventos del hijo. Para la fase de captura (de arriba hacia abajo), usa onClickCapture en lugar de onClick.
¿Las funciones flecha en onClick causan problemas de rendimiento?
En la mayoría de los casos, no. La función flecha en línea crea una nueva función en cada render, lo que puede anular React.memo en los componentes hijos. Optimiza con useCallback solo en rutas críticas como listas grandes.
¿Cómo escucho atajos de teclado o eventos a nivel de window?
Usa useEffect con addEventListener en window o document:
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === "Escape") closeModal();
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, []);¿Cómo tipo una prop de manejador que se pasa a un componente hijo?
Típala como una función que acepta los datos que necesitas:
interface Props {
onSearch: (query: string) => void;
}O usa el tipo de manejador integrado de React: React.MouseEventHandler<HTMLButtonElement>.
Relacionado
- Formularios — patrones de manejo de eventos específicos de formularios
- Componentes — pasar manejadores de eventos como props
- Refs — asociar listeners de eventos nativos cuando las props de React no son suficientes