Conceptos Básicos de Eventos en React
11 ejemplos para comenzar con Eventos en React: 7 básicos y 4 intermedios.
Requisitos previos
No se requieren paquetes adicionales: cada manejador en esta página viene con React. Un proyecto React estándar (Next.js, Vite o CRA) es suficiente.
Dos convenciones aplican a cada ejemplo:
- Los nombres de eventos usan camelCase en JSX:
onClick,onChange,onKeyDown— no en minúsculas. - Los manejadores reciben un evento sintético que normaliza las diferencias del navegador; su API coincide con el objeto nativo
Event(e.target,e.preventDefault(), etc.).
Los manejadores de eventos se ejecutan en el cliente. En un proyecto Next.js App Router, colócalos dentro de un componente marcado "use client" (o importa ese componente desde un Componente del Servidor).
Ejemplos Básicos
1. onClick
Responde a una pulsación de botón con un manejador de clic.
"use client";
export default function ClickCounter() {
const handleClick = () => {
alert("¡Se presionó el botón!");
};
return <button onClick={handleClick}>Haz clic en mí</button>;
}- Pasa una referencia de función (
onClick={handleClick}), no una llamada de función (onClick={handleClick()}). - El manejador recibe un
React.MouseEventconclientX,clientY,shiftKey, etc. - Los clics burbujean hacia arriba en el árbol: el
onClickde un padre también se dispara a menos que un hijo llame ae.stopPropagation(). - Usa
<button type="button">dentro de un<form>a menos que realmente quieras que envíe el formulario.
Relacionado: Eventos del Ratón -- clic, desplazamiento, arrastrar, menú contextual | Eventos (Fundamentos) -- descripción general de eventos sintéticos | Tipificación de Eventos --
MouseEvent<HTMLButtonElement>y similares
2. onChange en una Entrada de Texto
Captura pulsaciones de teclas con una entrada controlada.
"use client";
import { useState } from "react";
export default function NameInput() {
const [name, setName] = useState("");
return (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Tu nombre"
/>
<p>Hola, {name || "extraño"}!</p>
</div>
);
}onChangese dispara en cada pulsación de tecla — a diferencia delchangedel DOM nativo, que se dispara solo al perder el foco.e.target.valuees siempre una cadena; analiza números/booleanos explícitamente.- Una entrada controlada mantiene
valueen el estado de React — tienes control total sobre la validación y la transformación. - Para formularios grandes, prefiere
react-hook-formpara evitar un re-renderizado por pulsación de tecla.
Relacionado: Eventos de Formulario -- onChange, onInput, onSubmit | Formularios (Fundamentos) -- controlado vs. no controlado | React Hook Form -- escalabilidad de formularios más allá de algunos campos
3. onKeyDown
Responde a pulsaciones de teclas, incluidos modificadores y combinaciones de teclas.
"use client";
import { useState } from "react";
export default function SearchBox() {
const [query, setQuery] = useState("");
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
console.log("Buscar:", query);
} else if (e.key === "Escape") {
setQuery("");
}
};
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Buscar..."
/>
);
}- Usa
e.key(p. ej.,"Enter","Escape","ArrowUp") — no ele.keyCodeobsoleto. - Comprueba modificadores con
e.ctrlKey,e.metaKey,e.shiftKey,e.altKey— útil para atajos de teclado. - Para atajos globales (no vinculados a una entrada), adjunta un escuchador en
useEffectconwindow.addEventListener("keydown", ...). onKeyDownse dispara antes de que se inserte el carácter;onKeyUpse dispara después de soltar.
Relacionado: Eventos de Teclado -- atajos de teclado, composición IME, accesibilidad | useKeyboardShortcut -- gancho de atajo global reutilizable
4. onFocus y onBlur
Rastrrea cuándo un elemento gana o pierde el foco — para validación, tooltips y estado de la interfaz.
"use client";
import { useState } from "react";
export default function EmailField() {
const [email, setEmail] = useState("");
const [touched, setTouched] = useState(false);
const showError = touched && !email.includes("@");
return (
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onBlur={() => setTouched(true)}
/>
{showError && <p role="alert">Por favor, ingresa un correo válido.</p>}
</div>
);
}onFocusse dispara cuando el elemento recibe el foco;onBlurcuando lo pierde.- Las versiones de React de estos eventos burbujean — a diferencia del
focus/blurnativo. Úsalos en contenedores, no solo en entradas. - Patrón típico: espera a
onBlurantes de mostrar errores de validación para que los usuarios no sean asustados a mitad de la escritura. - Para captura de tabulación, cierre de menú y flujos de accesibilidad, combina con
relatedTargetnativo en el evento.
Relacionado: Eventos de Foco -- onFocus, onBlur, onFocusIn | Accesibilidad de Formularios -- patrones ARIA alrededor del foco
5. onMouseEnter y onMouseLeave
Construye interacciones de desplazamiento sin los problemas del mouseover nativo.
"use client";
import { useState } from "react";
export default function Tooltip() {
const [open, setOpen] = useState(false);
return (
<span
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
style={{ position: "relative" }}
>
Desplázate sobre mí
{open && (
<span style={{ position: "absolute", top: "-1.5rem", left: 0 }}>
¡Tooltip!
</span>
)}
</span>
);
}onMouseEnter/onMouseLeavese disparan una vez al entrar/salir del elemento — no se disparan para elementos secundarios, a diferencia deonMouseOver/onMouseOut.- Los dispositivos táctiles no disparan eventos de desplazamiento; proporciona una alternativa (toque, foco) para accesibilidad.
- Para el estado de desplazamiento que dura entre elementos secundarios, empareja con
onPointerEnter/onPointerLeavepara entrada unificada. - Prefiere CSS
:hoverpara estilos puramente visuales — reserva manejadores de JS para comportamiento.
Relacionado: Eventos del Ratón -- API completa del ratón | Eventos de Puntero -- ratón + toque + bolígrafo unificados | Componente Tooltip -- tooltip listo para producción
6. onSubmit
Maneja el envío de formulario sin una recarga completa de página.
"use client";
import { useState, type FormEvent } from "react";
export default function ContactForm() {
const [email, setEmail] = useState("");
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log("Enviar:", email);
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Enviar</button>
</form>
);
}- Llama a
e.preventDefault()para detener la recarga de página predeterminada del navegador. - Adjunta
onSubmital<form>, no al botón de envío — se dispara en clics de botón y pulsaciones de Enter. - Para el flujo de acción de formulario nativo (React 19+), usa
<form action={serverAction}>en su lugar y omiteonSubmit. - Incluye un botón
type="submit"real para que los usuarios de teclado puedan enviar con Enter.
Relacionado: Eventos de Formulario -- onSubmit, onReset, onInvalid | Formularios de Acciones del Servidor -- flujo de formulario React 19 nativo
7. onCopy y onPaste
Intercepta operaciones del portapapeles para transformar o bloquear el contenido.
"use client";
export default function ProtectedText() {
const handleCopy = (e: React.ClipboardEvent<HTMLDivElement>) => {
e.preventDefault();
e.clipboardData.setData("text/plain", "La copia está deshabilitada aquí.");
};
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
const pasted = e.clipboardData.getData("text");
console.log("Pegado:", pasted);
};
return (
<div onCopy={handleCopy}>
<p>Intenta copiar este texto.</p>
<input onPaste={handlePaste} placeholder="Pega aquí" />
</div>
);
}e.clipboardDataexponegetData(para pegar) ysetData(para copiar/cortar).- Llama a
e.preventDefault()antes desetDatapara reemplazar el contenido del portapapeles predeterminado. - Nunca uses esto para seguridad — los usuarios determinados aún pueden extraer texto del DOM.
- Para copiar texto mediante programación desde un botón, usa
navigator.clipboard.writeText()en su lugar.
Relacionado: Eventos del Portapapeles -- API completa de onCopy/onCut/onPaste | useCopyToClipboard -- gancho de botón "copiar" reutilizable
Ejemplos Intermedios
8. Eventos de Puntero para Entrada Unificada
Maneja ratón, toque y bolígrafo con un único conjunto de manejadores — sin bifurcación por tipo de entrada.
"use client";
import { useState } from "react";
export default function Draggable() {
const [pos, setPos] = useState({ x: 0, y: 0 });
const [dragging, setDragging] = useState(false);
const handlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
e.currentTarget.setPointerCapture(e.pointerId);
setDragging(true);
};
const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
if (dragging) setPos({ x: e.clientX, y: e.clientY });
};
const handlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
e.currentTarget.releasePointerCapture(e.pointerId);
setDragging(false);
};
return (
<div
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
style={{
position: "absolute",
left: pos.x,
top: pos.y,
width: 60,
height: 60,
background: dragging ? "crimson" : "steelblue",
touchAction: "none",
}}
/>
);
}- Los eventos de puntero se disparan para ratón, toque y bolígrafo — una ruta de código reemplaza manejadores separados de ratón y toque.
setPointerCapture(pointerId)sigue recibiendo eventospointermoveincluso si el cursor sale del elemento.- Establece
touch-action: noneen CSS para que el navegador no capture el gesto para desplazamiento/zoom. - Comprueba
e.pointerType("mouse","touch","pen") solo cuando la interacción realmente necesita bifurcarse.
Relacionado: Eventos de Puntero -- captura, eventos coalescidos, presión del bolígrafo | Eventos de Toque -- cuándo aún llegar a APIs específicas de toque | Eventos del Ratón -- contrapartida heredada
9. Desplazamiento Infinito con onScroll
Carga más contenido cuando el usuario llega al final de un contenedor desplazable.
"use client";
import { useState } from "react";
export default function FeedList({ initial }: { initial: string[] }) {
const [items, setItems] = useState(initial);
const [loading, setLoading] = useState(false);
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const el = e.currentTarget;
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 100;
if (atBottom && !loading) {
setLoading(true);
fetch(`/api/feed?after=${items.length}`)
.then((r) => r.json())
.then((next: string[]) => setItems((prev) => [...prev, ...next]))
.finally(() => setLoading(false));
}
};
return (
<div onScroll={handleScroll} style={{ height: 400, overflow: "auto" }}>
{items.map((item) => (
<p key={item}>{item}</p>
))}
{loading && <p>Cargando más...</p>}
</div>
);
}onScrollse dispara muy frecuentemente — protégete contra doble carga con una banderaloadingo un debounce.- Calcula "cerca del fondo" desde
scrollHeight - scrollTop - clientHeighten lugar de igualdad exacta para activar un poco antes. - Para código más limpio, usa
IntersectionObserveren un elemento centinela — se dispara solo cuando el centinela realmente entra en vista. - No adjuntes
onScrollawindowvía React — usauseEffect+window.addEventListener("scroll", ...)para desplazamiento a nivel de página.
Relacionado: Eventos de Desplazamiento -- throttling, encabezados pegajosos, restauración de desplazamiento | useIntersectionObserver -- patrón preferido para "cargar más"
10. Arrastrar y Soltar Nativo
Implementa arrastrar y soltar HTML5 entre dos listas usando eventos de arrastrar sintéticos de React.
"use client";
import { useState } from "react";
export default function Board() {
const [todo, setTodo] = useState(["Escribir documentos", "Enviar función"]);
const [done, setDone] = useState<string[]>([]);
const handleDragStart = (e: React.DragEvent<HTMLLIElement>, item: string) => {
e.dataTransfer.setData("text/plain", item);
};
const handleDrop = (e: React.DragEvent<HTMLUListElement>) => {
e.preventDefault();
const item = e.dataTransfer.getData("text/plain");
setTodo((prev) => prev.filter((i) => i !== item));
setDone((prev) => [...prev, item]);
};
return (
<div style={{ display: "flex", gap: "2rem" }}>
<ul>
{todo.map((item) => (
<li key={item} draggable onDragStart={(e) => handleDragStart(e, item)}>
{item}
</li>
))}
</ul>
<ul onDragOver={(e) => e.preventDefault()} onDrop={handleDrop}>
<strong>Hecho</strong>
{done.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}- Debes llamar a
e.preventDefault()enonDragOvero el destino de la suelta rechazará la suelta. - Pasa la carga útil a través de
e.dataTransfer.setData(type, value)y léela congetData— este es el único puente seguro entre ventanas. - El arrastrar y soltar nativo es engorroso en dispositivos móviles — para interacciones ricas o soporte táctil, recurre a
dnd-kito similar. - Agrega
draggableal elemento fuente; React no lo establece automáticamente.
Relacionado: Eventos de Arrastrar y Soltar -- onDragStart, onDragOver, onDrop | dnd-kit -- arrastrar y soltar ergonómico con soporte táctil
11. Eventos de Reproducción de Medios
Rastrrea cuándo un vídeo está listo para reproducirse y cuándo termina.
"use client";
import { useRef, useState } from "react";
export default function VideoPlayer({ src }: { src: string }) {
const ref = useRef<HTMLVideoElement>(null);
const [status, setStatus] = useState<"loading" | "ready" | "ended">("loading");
return (
<div>
<video
ref={ref}
src={src}
controls
onLoadedMetadata={() => setStatus("ready")}
onEnded={() => setStatus("ended")}
/>
<p>Estado: {status}</p>
{status === "ended" && (
<button onClick={() => ref.current?.play()}>Reproducir de nuevo</button>
)}
</div>
);
}onLoadedMetadatase dispara una vez que el navegador conoce la duración y dimensiones del vídeo — es seguro leerref.current.duration.onEndedse dispara cuando la reproducción alcanza el final;onPlay,onPause,onTimeUpdatecubren el resto del ciclo de vida.- Los eventos de medios no burbujean — adjúntalos directamente al elemento
<video>o<audio>. - Para transiciones CSS y animaciones de JS,
onTransitionEndyonAnimationEndte dan la misma señal de finalización.
Relacionado: Eventos de Medios y Animación -- referencia completa de eventos de medios + animación | useRef -- acceso al nodo DOM subyacente