Eventos de Ratón
Maneja clics, doble clic, pasos del ratón y movimiento del ratón en componentes React.
Referencia de Eventos de Ratón
| Prop React | Tipo TypeScript | Se Dispara Cuando |
|---|---|---|
onClick | React.MouseEvent<T> | El elemento se hace clic (mousedown + mouseup en el mismo objetivo) |
onDoubleClick | React.MouseEvent<T> | El elemento se hace doble clic |
onMouseDown | React.MouseEvent<T> | Se presiona el botón del ratón en el elemento |
onMouseUp | React.MouseEvent<T> | Se suelta el botón del ratón en el elemento |
onMouseEnter | React.MouseEvent<T> | El puntero entra al elemento (NO se propaga) |
onMouseLeave | React.MouseEvent<T> | El puntero sale del elemento (NO se propaga) |
onMouseOver | React.MouseEvent<T> | El puntero entra al elemento o a un hijo (se propaga) |
onMouseOut | React.MouseEvent<T> | El puntero sale del elemento o entra a un hijo (se propaga) |
onMouseMove | React.MouseEvent<T> | El puntero se mueve sobre el elemento |
onContextMenu | React.MouseEvent<T> | Se hace clic derecho o se presiona la tecla del menú contextual |
Receta
Tarjeta de referencia rápida -- lista para copiar y pegar.
// Manejador de clic con evento tipado
function ClickExample() {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log("Haz clic en", e.clientX, e.clientY);
};
return <button onClick={handleClick}>Haz clic en mí</button>;
}
// Pasar del ratón con onMouseEnter / onMouseLeave
function HoverExample() {
const [hovered, setHovered] = React.useState(false);
return (
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{ background: hovered ? "#e0e7ff" : "#fff" }}
>
Pasa del ratón por aquí
</div>
);
}
// Menú contextual de clic derecho
function ContextMenuExample() {
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
console.log("Menú personalizado en", e.clientX, e.clientY);
};
return <div onContextMenu={handleContextMenu}>Haz clic derecho en mí</div>;
}
// Doble clic
function DoubleClickExample() {
const handleDoubleClick = (e: React.MouseEvent<HTMLSpanElement>) => {
console.log("Doble clic");
};
return <span onDoubleClick={handleDoubleClick}>Doble clic para editar</span>;
}Cuándo usarlo: Necesitas responder a interacciones del ratón -- clics, pasos del ratón, clics derechos o rastreo de posición -- en un componente cliente.
Ejemplo Funcional
"use client";
import { useState, useRef, useCallback } from "react";
type ContextMenuState = {
visible: boolean;
x: number;
y: number;
};
export default function InteractiveCard() {
const [hovered, setHovered] = useState(false);
const [clickCount, setClickCount] = useState(0);
const [contextMenu, setContextMenu] = useState<ContextMenuState>({
visible: false,
x: 0,
y: 0,
});
const cardRef = useRef<HTMLDivElement>(null);
const handleClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
// Cierra el menú contextual en clic normal
if (contextMenu.visible) {
setContextMenu((prev) => ({ ...prev, visible: false }));
return;
}
setClickCount((c) => c + 1);
},
[contextMenu.visible]
);
const handleContextMenu = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
const rect = cardRef.current?.getBoundingClientRect();
if (!rect) return;
setContextMenu({
visible: true,
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
},
[]
);
const handleMenuAction = useCallback((action: string) => {
console.log("Acción de menú:", action);
setContextMenu((prev) => ({ ...prev, visible: false }));
}, []);
return (
<div
ref={cardRef}
onClick={handleClick}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => {
setHovered(false);
setContextMenu((prev) => ({ ...prev, visible: false }));
}}
onContextMenu={handleContextMenu}
style={{
position: "relative",
padding: "24px",
border: `2px solid ${hovered ? "#6366f1" : "#e5e7eb"}`,
borderRadius: "12px",
background: hovered ? "#f5f3ff" : "#fff",
transition: "all 150ms ease",
cursor: "pointer",
userSelect: "none",
}}
>
<h3>Tarjeta Interactiva</h3>
<p>Haz clic {clickCount} veces. Haz clic derecho para menú contextual.</p>
{contextMenu.visible && (
<ul
style={{
position: "absolute",
top: contextMenu.y,
left: contextMenu.x,
background: "#fff",
border: "1px solid #e5e7eb",
borderRadius: "8px",
boxShadow: "0 4px 12px rgba(0,0,0,0.1)",
listStyle: "none",
padding: "4px 0",
margin: 0,
zIndex: 10,
}}
>
{["Editar", "Duplicar", "Eliminar"].map((action) => (
<li
key={action}
onClick={(e) => {
e.stopPropagation();
handleMenuAction(action);
}}
onMouseEnter={(e) =>
(e.currentTarget.style.background = "#f3f4f6")
}
onMouseLeave={(e) =>
(e.currentTarget.style.background = "transparent")
}
style={{ padding: "8px 16px", cursor: "pointer" }}
>
{action}
</li>
))}
</ul>
)}
</div>
);
}Lo que esto demuestra:
- Combinación de
onClick,onMouseEnter,onMouseLeaveyonContextMenuen un solo elemento - Posicionamiento de un menú contextual personalizado relativo a la tarjeta usando
getBoundingClientRect - Usando
e.stopPropagation()para prevenir que los clics de elementos del menú disparen el manejador de clic de la tarjeta - Usando
e.preventDefault()enonContextMenupara suprimir el menú predeterminado del navegador - Tipado de TypeScript para estado, refs y manejadores de eventos
Inmersión Profunda
Cómo Funciona
- React normaliza eventos de ratón a través de su sistema de Eventos Sintéticos. Cada manejador de eventos de ratón recibe un
React.MouseEvent<T>que envuelve elMouseEventnativo con una interfaz consistente entre navegadores. - React usa delegación de eventos -- un único listener se adjunta en la raíz, no en cada elemento. Tu manejador se invoca durante la fase de burbuja por defecto.
onMouseEnteryonMouseLeaveNO se propagan. Se disparan solo para el elemento exacto, no sus hijos. UsaonMouseOveryonMouseOutsi necesitas comportamiento de propagación.- El Evento Sintético se agrupa y se reutiliza. Si necesitas acceder al evento de forma asincrónica (por ejemplo, en un
setTimeout), lee primero los valores que necesitas en variables locales.
Variaciones
Clic con paso de datos:
type Item = { id: string; name: string };
function ItemList({ items }: { items: Item[] }) {
const handleClick = (item: Item) => (e: React.MouseEvent<HTMLLIElement>) => {
console.log("Seleccionado:", item.id, "en", e.clientX);
};
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={handleClick(item)}>
{item.name}
</li>
))}
</ul>
);
}Rastreo de pasos del ratón con coordenadas:
function HoverTracker() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect();
setPosition({
x: e.clientX - rect.left,
y: e.clientY - rect.top,
});
},
[]
);
return (
<div onMouseMove={handleMouseMove} style={{ width: 300, height: 300 }}>
Cursor en ({position.x}, {position.y})
</div>
);
}Detección de arrastre inicial (mousedown + threshold de mousemove):
function DragDetector() {
const startPos = useRef<{ x: number; y: number } | null>(null);
const [dragging, setDragging] = useState(false);
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
startPos.current = { x: e.clientX, y: e.clientY };
};
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!startPos.current) return;
const dx = e.clientX - startPos.current.x;
const dy = e.clientY - startPos.current.y;
if (Math.sqrt(dx * dx + dy * dy) > 5) {
setDragging(true);
}
};
const handleMouseUp = () => {
startPos.current = null;
setDragging(false);
};
return (
<div
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
>
{dragging ? "Arrastrando..." : "Haz clic y arrastra"}
</div>
);
}Detección de pulsación larga:
function LongPressButton({ onLongPress }: { onLongPress: () => void }) {
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleMouseDown = () => {
timerRef.current = setTimeout(onLongPress, 600);
};
const handleMouseUp = () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
return (
<button onMouseDown={handleMouseDown} onMouseUp={handleMouseUp}>
Pulsación larga
</button>
);
}Menú contextual de clic derecho con portal:
import { createPortal } from "react-dom";
function ContextMenuPortal({
x,
y,
onClose,
}: {
x: number;
y: number;
onClose: () => void;
}) {
return createPortal(
<div
style={{ position: "fixed", top: y, left: x, zIndex: 9999 }}
onClick={onClose}
>
<ul style={{ background: "#fff", border: "1px solid #ddd", padding: 8 }}>
<li>Copiar</li>
<li>Pegar</li>
</ul>
</div>,
document.body
);
}Notas de TypeScript
// El genérico T especifica el tipo de elemento para e.currentTarget
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.currentTarget; // HTMLButtonElement -- el elemento al que se adjunta el manejador
e.target; // EventTarget -- el elemento realmente hecho clic (podría ser un hijo)
};
// Usa HTMLElement como fallback genérico cuando el elemento exacto varía
const handleAny = (e: React.MouseEvent<HTMLElement>) => {
e.currentTarget.dataset.id; // funciona para cualquier elemento HTML
};
// Estrechamiento de e.target (se tipifica como EventTarget, no Element)
const handleDelegated = (e: React.MouseEvent<HTMLUListElement>) => {
const target = e.target as HTMLElement;
if (target.tagName === "LI") {
console.log(target.textContent);
}
};
// Detección de botón del ratón
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
// e.button: 0 = izquierda, 1 = centro, 2 = derecha
if (e.button === 2) console.log("Clic derecho");
};
// Coordenadas disponibles en cada evento de ratón
// e.clientX / e.clientY -- relativo a viewport
// e.pageX / e.pageY -- relativo a documento
// e.screenX / e.screenY -- relativo a monitor
// e.nativeEvent.offsetX / offsetY -- relativo a elemento objetivoProblemas
-
onMouseEnter/onMouseLeavevsonMouseOver/onMouseOut--onMouseEnteryonMouseLeaveNO se propagan y NO se disparan al moverse entre elementos hijos.onMouseOveryonMouseOutSÍ se propagan, por lo que se disparan al entrar/salir de hijos también. Usar el par incorrecto y los estados de pasos del ratón parpadearán en elementos anidados. -
Crear nuevas funciones flecha en JSX en cada renderizado -- Escribir
onClick={() => handleClick(id)}crea una nueva referencia de función cada renderizado, lo cual puede causar re-renderizados innecesarios en componentes hijos que dependen de igualdad referencial. Solución: UsauseCallbacko extrae el manejador a una referencia estable. Para listas, considera un componente hijo que reciba el elemento y adjunte su propio manejador. -
onClickse dispara en CUALQUIER botón del ratón por defecto -- ElonClickde React se dispara solo para clics izquierdos en la mayoría de navegadores, pero no se garantiza para todos los elementos y navegadores. Si necesitas distinguir botones, verificae.buttonenonMouseDownuonMouseUp. -
Confusión entre
e.targetye.currentTarget--e.targetes el elemento que fue realmente hecho clic (podría ser un hijo).e.currentTargetes el elemento al que se adjunta el manejador. Si haces clic en un<span>dentro de un<button>,e.targetes el span,e.currentTargetes el button. Acceder a propiedades como.valueo.datasetene.targetrequiere estrechamiento de tipo. -
onDoubleClickse dispara dos eventosonClickprimero -- Un doble clic desencadena:onClick->onClick->onDoubleClick. Si tu manejadoronClickrealiza una acción, obtendrás esa acción dos veces antes de que se dispare el doble clic. Solución: Usa un temporizador de debounce para distinguir clics simples de dobles, o evita combinar ambos en el mismo elemento. -
Los eventos sintéticos se anulan después del manejador -- Acceder a
e.clientXdentro de unsetTimeouto después de unawaitdevuelvenullporque React recicla el objeto de evento. Solución: Lee valores en variables locales antes de cualquier operación asincrónica:const x = e.clientX;. -
onMouseMovese dispara muy frecuentemente -- Adjuntar actualizaciones de estado aonMouseMovepuede causar cientos de re-renderizados por segundo. Solución: Throttle conrequestAnimationFrameo una utilidad de throttle, o usa una ref para almacenar posición y solo re-renderiza cuando sea necesario.
Alternativas
| Alternativa | Úsalo Cuando | No lo Uses Cuando |
|---|---|---|
Pointer Events (onPointerDown, etc.) | Necesitas soportar touch, pen y mouse de forma uniforme | Solo apuntas a mouse en desktop |
CSS :hover | Solo estilos hover, no se necesita lógica JS | Necesitas rastrear estado de hover o coordenadas en JS |
Drag and Drop API (onDragStart, etc.) | Necesitas arrastrar y soltar completo con transferencia de datos | Solo necesitas detección de movimiento al hacer clic y arrastrar |
addEventListener en useEffect | Necesitas listeners globales (por ejemplo, rastreo de ratón a nivel de documento) | El evento está limitado a un elemento específico |
| Librerías de terceros (dnd-kit, Framer Motion) | Arrastrar complejo, gestos o animación vinculada a la posición del ratón | Manejadores simples de clic o pasos del ratón |
Preguntas Frecuentes
¿Cuál es la diferencia entre onMouseEnter/onMouseLeave y onMouseOver/onMouseOut?
onMouseEnter/onMouseLeaveNO se propagan y se disparan solo para el elemento exacto, no sus hijosonMouseOver/onMouseOutSÍ se propagan, por lo que se disparan al entrar/salir de elementos hijos también- Usa
Enter/Leavepara estado de pasos del ratón en un solo elemento; usaOver/Outcuando necesites propagación
¿Cuál es la diferencia entre e.target y e.currentTarget?
e.targetes el elemento que fue realmente hecho clic (podría ser un hijo anidado)e.currentTargetes el elemento al que se adjunta el manejador- Si haces clic en un
<span>dentro de un<button>,e.targetes el span,e.currentTargetes el button
¿Cómo detectas qué botón del ratón se presionó?
Usa e.button en onMouseDown u onMouseUp:
0= botón izquierdo1= botón central2= botón derecho
No confíes en onClick para detección de botones -- es principalmente para clics izquierdos.
¿Cómo implementas un menú contextual de clic derecho personalizado?
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault(); // suprime menú predeterminado del navegador
setMenu({ visible: true, x: e.clientX, y: e.clientY });
};
return <div onContextMenu={handleContextMenu}>Haz clic derecho en mí</div>;Problema: ¿Por qué onDoubleClick también dispara onClick dos veces?
Un doble clic desencadena: onClick -> onClick -> onDoubleClick. Si tu onClick realiza una acción, se ejecuta dos veces antes de que se dispare el doble clic. Usa un temporizador de debounce para distinguir clics simples de dobles, o evita combinar ambos manejadores en el mismo elemento.
¿Por qué no puedes acceder a e.clientX dentro de un setTimeout o después de un await?
Los eventos sintéticos de React se anulan después de que el manejador regresa. Acceder a propiedades más tarde da null. Lee valores en variables locales antes de cualquier operación asincrónica: const x = e.clientX;.
¿Cómo pasas datos a un manejador de clic en una lista sin crear una nueva función en cada renderizado?
function ItemList({ items }: { items: Item[] }) {
const handleClick = (item: Item) =>
(e: React.MouseEvent<HTMLLIElement>) => {
console.log("Seleccionado:", item.id);
};
return (
<ul>
{items.map((item) => (
<li key={item.id} onClick={handleClick(item)}>{item.name}</li>
))}
</ul>
);
}Para mejor desempeño, extrae cada elemento en un componente hijo con su propio manejador estable.
Problema: ¿Por qué onMouseMove causa problemas de desempeño?
onMouseMove se dispara muy frecuentemente (cientos de veces por segundo). Adjuntar actualizaciones de estado a él causa re-renderizados excesivos. Throttle con requestAnimationFrame, usa una utilidad de throttle, o almacena posición en una ref y solo re-renderiza cuando sea necesario.
¿Cómo implementas detección de pulsación larga con eventos de ratón?
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleMouseDown = () => {
timerRef.current = setTimeout(onLongPress, 600);
};
const handleMouseUp = () => {
if (timerRef.current) clearTimeout(timerRef.current);
};¿Qué propiedades de coordenadas están disponibles en eventos de ratón y cómo difieren?
clientX/clientY-- relativo al viewportpageX/pageY-- relativo al documento (incluye offset de desplazamiento)screenX/screenY-- relativo al monitornativeEvent.offsetX/offsetY-- relativo al elemento objetivo
¿Cuál es el tipo genérico de TypeScript correcto para manejadores de eventos de ratón?
// Tipo de elemento específico
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.currentTarget; // HTMLButtonElement
};
// Fallback genérico para cualquier elemento HTML
const handleAny = (e: React.MouseEvent<HTMLElement>) => {
e.currentTarget.dataset.id; // funciona para cualquier elemento
};¿Cómo estrechas el tipo de e.target al usar delegación de eventos en TypeScript?
const handleDelegated = (e: React.MouseEvent<HTMLUListElement>) => {
// e.target se tipifica como EventTarget, no Element
const target = e.target as HTMLElement;
if (target.tagName === "LI") {
console.log(target.textContent);
}
};Relacionado
- Eventos de Teclado -- Respondiendo a pulsaciones de tecla y atajos de teclado
- Eventos de Formulario -- Manejo de envíos de formulario y cambios de entrada