Eventos de Ratón
Maneja clics, doble clic, pasos del ratón y movimiento del ratón en componentes React.
Busca en todas las páginas de la documentación
Maneja clics, doble clic, pasos del ratón y movimiento del ratón en componentes React.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
| 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 |
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.
"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:
onClick, onMouseEnter, onMouseLeave y onContextMenu en un solo elementogetBoundingClientRecte.stopPropagation() para prevenir que los clics de elementos del menú disparen el manejador de clic de la tarjetae.preventDefault() en onContextMenu para suprimir el menú predeterminado del navegadorReact.MouseEvent<T> que envuelve el MouseEvent nativo con una interfaz consistente entre navegadores.onMouseEnter y onMouseLeave NO se propagan. Se disparan solo para el elemento exacto, no sus hijos. Usa onMouseOver y onMouseOut si necesitas comportamiento de propagación.setTimeout), lee primero los valores que necesitas en variables locales.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
);
}// 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 objetivoonMouseEnter/onMouseLeave vs onMouseOver/onMouseOut -- onMouseEnter y onMouseLeave NO se propagan y NO se disparan al moverse entre elementos hijos. onMouseOver y onMouseOut SÍ 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: Usa useCallback o extrae el manejador a una referencia estable. Para listas, considera un componente hijo que reciba el elemento y adjunte su propio manejador.
onClick se dispara en CUALQUIER botón del ratón por defecto -- El onClick de 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, verifica e.button en onMouseDown u onMouseUp.
Confusión entre e.target y e.currentTarget -- e.target es el elemento que fue realmente hecho clic (podría ser un hijo). e.currentTarget es el elemento al que se adjunta el manejador. Si haces clic en un <span> dentro de un <button>, e.target es el span, e.currentTarget es el button. Acceder a propiedades como .value o .dataset en e.target requiere estrechamiento de tipo.
onDoubleClick se dispara dos eventos onClick primero -- Un doble clic desencadena: onClick -> onClick -> onDoubleClick. Si tu manejador onClick realiza 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.clientX dentro de un setTimeout o después de un await devuelve null porque React recicla el objeto de evento. Solución: Lee valores en variables locales antes de cualquier operación asincrónica: const x = e.clientX;.
onMouseMove se dispara muy frecuentemente -- Adjuntar actualizaciones de estado a onMouseMove puede causar cientos de re-renderizados por segundo. Solución: Throttle con requestAnimationFrame o una utilidad de throttle, o usa una ref para almacenar posición y solo re-renderiza cuando sea necesario.
| 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 |
onMouseEnter / onMouseLeave NO se propagan y se disparan solo para el elemento exacto, no sus hijosonMouseOver / onMouseOut SÍ se propagan, por lo que se disparan al entrar/salir de elementos hijos tambiénEnter/Leave para estado de pasos del ratón en un solo elemento; usa Over/Out cuando necesites propagacióne.target es el elemento que fue realmente hecho clic (podría ser un hijo anidado)e.currentTarget es el elemento al que se adjunta el manejador<span> dentro de un <button>, e.target es el span, e.currentTarget es el buttonUsa e.button en onMouseDown u onMouseUp:
0 = botón izquierdo1 = botón central2 = botón derechoNo confíes en onClick para detección de botones -- es principalmente para clics izquierdos.
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>;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.
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;.
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.
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.
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleMouseDown = () => {
timerRef.current = setTimeout(onLongPress, 600);
};
const handleMouseUp = () => {
if (timerRef.current) clearTimeout(timerRef.current);
};clientX / clientY -- relativo al viewportpageX / pageY -- relativo al documento (incluye offset de desplazamiento)screenX / screenY -- relativo al monitornativeEvent.offsetX / offsetY -- relativo al elemento objetivo// 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
};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);
}
};Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥