React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

mouse-eventsclickhovercontext-menudragreact-19typescript

Mouse Events

Handle clicks, double-clicks, hovers, and mouse movement in React components.

Mouse Event Reference

React PropTypeScript TypeFires When
onClickReact.MouseEvent<T>Element is clicked (mousedown + mouseup on same target)
onDoubleClickReact.MouseEvent<T>Element is double-clicked
onMouseDownReact.MouseEvent<T>Mouse button is pressed down on element
onMouseUpReact.MouseEvent<T>Mouse button is released on element
onMouseEnterReact.MouseEvent<T>Pointer enters the element (does NOT bubble)
onMouseLeaveReact.MouseEvent<T>Pointer leaves the element (does NOT bubble)
onMouseOverReact.MouseEvent<T>Pointer enters the element or a child (bubbles)
onMouseOutReact.MouseEvent<T>Pointer leaves the element or enters a child (bubbles)
onMouseMoveReact.MouseEvent<T>Pointer moves while over the element
onContextMenuReact.MouseEvent<T>Right-click or context menu key is pressed

Recipe

Quick-reference recipe card -- copy-paste ready.

// Click handler with typed event
function ClickExample() {
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log("Clicked at", e.clientX, e.clientY);
  };
 
  return <button onClick={handleClick}>Click me</button>;
}
 
// Hover with onMouseEnter / onMouseLeave
function HoverExample() {
  const [hovered, setHovered] = React.useState(false);
 
  return (
    <div
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      style={{ background: hovered ? "#e0e7ff" : "#fff" }}
    >
      Hover me
    </div>
  );
}
 
// Right-click context menu
function ContextMenuExample() {
  const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
    e.preventDefault();
    console.log("Custom menu at", e.clientX, e.clientY);
  };
 
  return <div onContextMenu={handleContextMenu}>Right-click me</div>;
}
 
// Double-click
function DoubleClickExample() {
  const handleDoubleClick = (e: React.MouseEvent<HTMLSpanElement>) => {
    console.log("Double-clicked");
  };
 
  return <span onDoubleClick={handleDoubleClick}>Double-click to edit</span>;
}

When to reach for this: You need to respond to mouse interactions -- clicks, hovers, right-clicks, or position tracking -- in a client component.

Working Example

"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>) => {
      // Close context menu on regular click
      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("Menu action:", 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>Interactive Card</h3>
      <p>Clicked {clickCount} times. Right-click for context menu.</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,
          }}
        >
          {["Edit", "Duplicate", "Delete"].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>
  );
}

What this demonstrates:

  • Combining onClick, onMouseEnter, onMouseLeave, and onContextMenu on a single element
  • Positioning a custom context menu relative to the card using getBoundingClientRect
  • Using e.stopPropagation() to prevent menu item clicks from triggering the card click handler
  • Using e.preventDefault() on onContextMenu to suppress the browser default menu
  • TypeScript typing for state, refs, and event handlers

Deep Dive

How It Works

  • React normalizes mouse events through its Synthetic Event system. Every mouse event handler receives a React.MouseEvent<T> that wraps the native MouseEvent with a consistent cross-browser interface.
  • React uses event delegation -- a single listener is attached at the root, not on each element. Your handler is invoked during the bubble phase by default.
  • onMouseEnter and onMouseLeave do NOT bubble. They fire only for the exact element, not its children. Use onMouseOver and onMouseOut if you need bubbling behavior.
  • The Synthetic Event is pooled and reused. If you need to access the event asynchronously (e.g., in a setTimeout), read the values you need into local variables first.

Variations

Click with data passing:

type Item = { id: string; name: string };
 
function ItemList({ items }: { items: Item[] }) {
  const handleClick = (item: Item) => (e: React.MouseEvent<HTMLLIElement>) => {
    console.log("Selected:", item.id, "at", e.clientX);
  };
 
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id} onClick={handleClick(item)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

Hover tracking with coordinates:

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 at ({position.x}, {position.y})
    </div>
  );
}

Drag-start detection (mousedown + mousemove threshold):

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 ? "Dragging..." : "Click and drag"}
    </div>
  );
}

Long press detection:

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}>
      Long press me
    </button>
  );
}

Right-click context menu with 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>Copy</li>
        <li>Paste</li>
      </ul>
    </div>,
    document.body
  );
}

TypeScript Notes

// The generic T specifies the element type for e.currentTarget
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  e.currentTarget; // HTMLButtonElement -- the element the handler is attached to
  e.target;        // EventTarget -- the actual element clicked (could be a child)
};
 
// Use HTMLElement as a generic fallback when the exact element varies
const handleAny = (e: React.MouseEvent<HTMLElement>) => {
  e.currentTarget.dataset.id; // works for any HTML element
};
 
// Narrowing e.target (it is typed as EventTarget, not Element)
const handleDelegated = (e: React.MouseEvent<HTMLUListElement>) => {
  const target = e.target as HTMLElement;
  if (target.tagName === "LI") {
    console.log(target.textContent);
  }
};
 
// Mouse button detection
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
  // e.button: 0 = left, 1 = middle, 2 = right
  if (e.button === 2) console.log("Right click");
};
 
// Coordinates available on every mouse event
// e.clientX / e.clientY -- relative to viewport
// e.pageX / e.pageY -- relative to document
// e.screenX / e.screenY -- relative to monitor
// e.nativeEvent.offsetX / offsetY -- relative to target element

Gotchas

  • onMouseEnter/onMouseLeave vs onMouseOver/onMouseOut -- onMouseEnter and onMouseLeave do NOT bubble and do NOT fire when moving between child elements. onMouseOver and onMouseOut DO bubble, so they fire when entering/leaving children too. Use the wrong pair and hover states will flicker on nested elements.

  • Creating new arrow functions in JSX on every render -- Writing onClick={() => handleClick(id)} creates a new function reference each render, which can cause unnecessary re-renders in child components that rely on referential equality. Fix: Use useCallback or extract the handler to a stable reference. For lists, consider a child component that receives the item and attaches its own handler.

  • onClick fires on ANY mouse button by default -- React's onClick fires for left-clicks only in most browsers, but this is not guaranteed for all elements and browsers. If you need to distinguish buttons, check e.button in onMouseDown or onMouseUp.

  • e.target vs e.currentTarget confusion -- e.target is the element that was actually clicked (could be a child). e.currentTarget is the element the handler is attached to. If you click a <span> inside a <button>, e.target is the span, e.currentTarget is the button. Accessing properties like .value or .dataset on e.target requires type narrowing.

  • onDoubleClick fires two onClick events first -- A double-click triggers: onClick -> onClick -> onDoubleClick. If your onClick handler performs an action, you will get that action twice before the double-click fires. Fix: Use a debounce timer to distinguish single from double clicks, or avoid combining both on the same element.

  • Synthetic events are nullified after the handler -- Accessing e.clientX inside a setTimeout or after an await returns null because React recycles the event object. Fix: Read values into local variables before any async operation: const x = e.clientX;.

  • onMouseMove fires very frequently -- Attaching state updates to onMouseMove can cause hundreds of re-renders per second. Fix: Throttle with requestAnimationFrame or a throttle utility, or use a ref to store position and only re-render when needed.

Alternatives

AlternativeUse WhenDon't Use When
Pointer Events (onPointerDown, etc.)You need to support touch, pen, and mouse uniformlyYou only target mouse on desktop
CSS :hoverHover styles only, no JS logic neededYou need to track hover state or coordinates in JS
Drag and Drop API (onDragStart, etc.)You need full drag-and-drop with data transferYou just need click-and-drag movement detection
addEventListener in useEffectYou need global listeners (e.g., document-level mouse tracking)The event is scoped to a specific element
Third-party libraries (dnd-kit, Framer Motion)Complex drag, gestures, or animation tied to mouse positionSimple click or hover handlers

FAQs

What is the difference between onMouseEnter/onMouseLeave and onMouseOver/onMouseOut?
  • onMouseEnter / onMouseLeave do NOT bubble and fire only for the exact element, not its children
  • onMouseOver / onMouseOut DO bubble, so they fire when entering/leaving child elements too
  • Use Enter/Leave for hover state on a single element; use Over/Out when you need bubbling
What is the difference between e.target and e.currentTarget?
  • e.target is the element that was actually clicked (could be a nested child)
  • e.currentTarget is the element the handler is attached to
  • If you click a <span> inside a <button>, e.target is the span, e.currentTarget is the button
How do you detect which mouse button was pressed?

Use e.button in onMouseDown or onMouseUp:

  • 0 = left button
  • 1 = middle button
  • 2 = right button

Do not rely on onClick for button detection -- it is primarily for left-clicks.

How do you implement a custom right-click context menu?
const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
  e.preventDefault(); // suppress browser default menu
  setMenu({ visible: true, x: e.clientX, y: e.clientY });
};
return <div onContextMenu={handleContextMenu}>Right-click me</div>;
Gotcha: Why does onDoubleClick also fire onClick twice?

A double-click triggers: onClick -> onClick -> onDoubleClick. If your onClick performs an action, it runs twice before the double-click fires. Use a debounce timer to distinguish single from double clicks, or avoid combining both handlers on the same element.

Why can't you access e.clientX inside a setTimeout or after an await?

React's synthetic events are nullified after the handler returns. Accessing properties later gives null. Read values into local variables before any async operation: const x = e.clientX;.

How do you pass data to a click handler in a list without creating a new function on every render?
function ItemList({ items }: { items: Item[] }) {
  const handleClick = (item: Item) =>
    (e: React.MouseEvent<HTMLLIElement>) => {
      console.log("Selected:", item.id);
    };
 
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id} onClick={handleClick(item)}>{item.name}</li>
      ))}
    </ul>
  );
}

For better performance, extract each item into a child component with its own stable handler.

Gotcha: Why does onMouseMove cause performance issues?

onMouseMove fires very frequently (hundreds of times per second). Attaching state updates to it causes excessive re-renders. Throttle with requestAnimationFrame, use a throttle utility, or store position in a ref and only re-render when needed.

How do you implement long press detection with mouse events?
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleMouseDown = () => {
  timerRef.current = setTimeout(onLongPress, 600);
};
const handleMouseUp = () => {
  if (timerRef.current) clearTimeout(timerRef.current);
};
What coordinate properties are available on mouse events, and how do they differ?
  • clientX / clientY -- relative to the viewport
  • pageX / pageY -- relative to the document (includes scroll offset)
  • screenX / screenY -- relative to the monitor
  • nativeEvent.offsetX / offsetY -- relative to the target element
What is the correct TypeScript generic type for mouse event handlers?
// Specific element type
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  e.currentTarget; // HTMLButtonElement
};
 
// Generic fallback for any HTML element
const handleAny = (e: React.MouseEvent<HTMLElement>) => {
  e.currentTarget.dataset.id; // works for any element
};
How do you narrow the type of e.target when using event delegation in TypeScript?
const handleDelegated = (e: React.MouseEvent<HTMLUListElement>) => {
  // e.target is typed as EventTarget, not Element
  const target = e.target as HTMLElement;
  if (target.tagName === "LI") {
    console.log(target.textContent);
  }
};