Event Handling đŸ–ąī¸

1. Introduction 👋

Interactivity is at the heart of every modern web application, and event handling is how React responds to user actions like clicks, key presses, and form submissions. This tutorial covers React's event system in depth — from basic handlers to advanced patterns like propagation, delegation, and TS typing.

Information

This tutorial builds on JSX, Components, and State. Reviewing those first will help you get the most out of this guide.

2. What are Events? 🤔

An event is an action that occurs in the browser — a user clicking a button, typing in a field, submitting a form, or resizing a window. Event-driven programming allows applications to react to these actions as they happen.

  • User-initiated: Clicks, key presses, form submissions, scrolling.
  • Browser-initiated: Page load, resize, network status changes.
  • Media-related: Video play, pause, or end events.

3. React Event System âš›ī¸

React implements its own event system layered on top of native browser events. Instead of attaching listeners to individual DOM nodes, React attaches a single listener at the root of the application and manages dispatching internally for performance and consistency.

Code Snippet

function Button() {
  function handleClick() {
    console.log("Button was clicked!");
  }

  return <button onClick={handleClick}>Click Me</button>;
}

Note

React's event system provides a consistent API across browsers, abstracting away many cross-browser inconsistencies found in raw DOM events.

4. Synthetic Events đŸ§Ŧ

React wraps native browser events in a cross-browser wrapper called a SyntheticEvent. It has the same interface as native events (stopPropagation(), preventDefault(), etc.) but behaves consistently across all browsers.

Code Snippet

function Input() {
  function handleChange(event) {
    console.log(event.target.value); // SyntheticEvent
  }

  return <input onChange={handleChange} />;
}

Important

Since React 17+, synthetic events are no longer pooled (reused and nulled out after the handler). You can safely access event properties asynchronously without extra steps.

5. Event Handlers đŸŽ›ī¸

An event handler is a function that runs in response to a specific event. In JSX, handlers are passed as function references to camelCase event props.

Code Snippet

function SubmitButton() {
  function handleSubmit() {
    console.log("Form submitted!");
  }

  return <button onClick={handleSubmit}>Submit</button>;
}

Caution

Pass the function reference (onClick={handleSubmit}), not the result of calling it (onClick={handleSubmit()}), or it will run immediately during render.

6. Binding Event Handlers 🔗

In function components with Hooks, this binding issues from class components don't apply. Handlers defined as regular functions or arrow functions inside the component automatically have access to the correct scope.

Code Snippet

function Toggle() {
  const [isOn, setIsOn] = useState(false);

  // No binding needed — closures handle scope naturally
  function handleToggle() {
    setIsOn(!isOn);
  }

  return <button onClick={handleToggle}>{isOn ? "ON" : "OFF"}</button>;
}

Note

Binding with .bind(this) or constructor binding was only necessary in class components — function components avoid this complexity entirely.

7. Passing Arguments to Event Handlers 📨

To pass custom arguments to an event handler, wrap the call in an arrow function so it's only invoked when the event actually fires.

Code Snippet

function TodoItem({ id, text, onDelete }) {
  return (
    <li>
      {text}
      <button onClick={() => onDelete(id)}>Delete</button>
    </li>
  );
}

Tip

If you need the event object and a custom argument, include both: onClick={(e) => handleClick(id, e)}.

8. Mouse Events đŸ–ąī¸

EventFires When
onClickElement is clicked
onDoubleClickElement is double-clicked
onMouseEnter / onMouseLeaveCursor enters or leaves an element (no bubbling)
onMouseOver / onMouseOutCursor enters or leaves, including child elements (bubbles)
onMouseDown / onMouseUpMouse button pressed or released

Code Snippet

function HoverCard() {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <div
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
    >
      {isHovered ? "👋 Hovering!" : "Hover over me"}
    </div>
  );
}

9. Keyboard Events âŒ¨ī¸

EventFires When
onKeyDownA key is pressed down
onKeyUpA key is released
onKeyPressA character key is pressed (deprecated in favor of onKeyDown)

Code Snippet

function SearchInput() {
  function handleKeyDown(event) {
    if (event.key === "Enter") {
      console.log("Search triggered!");
    }
  }

  return <input onKeyDown={handleKeyDown} placeholder="Search..." />;
}

Caution

onKeyPress is deprecated — use onKeyDown and check event.key instead.

10. Form Events 📝

EventFires When
onChangeAn input's value changes
onSubmitA form is submitted
onFocus / onBlurAn input gains or loses focus
onInvalidA form field fails validation

Code Snippet

function LoginForm() {
  const [email, setEmail] = useState("");

  function handleSubmit(event) {
    event.preventDefault();
    console.log("Submitting:", email);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        type="email"
      />
      <button type="submit">Log In</button>
    </form>
  );
}

Important

Always call event.preventDefault() in onSubmit to stop the browser's default full-page reload behavior.

11. Clipboard Events 📋

EventFires When
onCopyContent is copied
onCutContent is cut
onPasteContent is pasted

Code Snippet

function SecureInput() {
  function handlePaste(event) {
    event.preventDefault();
    console.log("Pasting is disabled for this field.");
  }

  return <input onPaste={handlePaste} />;
}

12. Focus Events đŸŽ¯

Code Snippet

function ValidatedInput() {
  const [touched, setTouched] = useState(false);

  return (
    <input
      onFocus={() => console.log("Input focused")}
      onBlur={() => setTouched(true)}
    />
  );
}

Note

Unlike native focus/blur events, React's onFocus and onBlur bubble, making them easier to handle at a parent container level.

13. Touch Events 👆

EventFires When
onTouchStartA touch point is placed on the screen
onTouchMoveA touch point moves
onTouchEndA touch point is removed

Code Snippet

function SwipeCard() {
  function handleTouchStart(event) {
    console.log("Touch started at", event.touches[0].clientX);
  }

  return <div onTouchStart={handleTouchStart}>Swipe me</div>;
}

Tip

For robust cross-device gesture support, consider Pointer Events instead, which unify mouse, touch, and stylus input.

14. Pointer Events 👉

Pointer events unify mouse, touch, and pen input under a single, consistent API — making them the recommended choice for modern cross-device interactions.

Code Snippet

function Draggable() {
  function handlePointerDown(event) {
    console.log("Pointer type:", event.pointerType); // "mouse", "touch", or "pen"
  }

  return <div onPointerDown={handlePointerDown}>Drag me</div>;
}

15. Drag and Drop Events đŸŽ¯

EventFires When
onDragStartDragging begins
onDragOverA dragged item is over a valid drop target
onDropAn item is dropped
onDragEndDragging ends

Code Snippet

function DropZone() {
  function handleDragOver(event) {
    event.preventDefault(); // required to allow dropping
  }

  function handleDrop(event) {
    event.preventDefault();
    console.log("Dropped:", event.dataTransfer.getData("text"));
  }

  return (
    <div onDragOver={handleDragOver} onDrop={handleDrop}>
      Drop files here
    </div>
  );
}

Important

You must call event.preventDefault() in onDragOver, or the onDrop event will never fire.

16. Media Events đŸŽŦ

EventFires When
onPlay / onPauseMedia starts or pauses playback
onEndedPlayback reaches the end
onVolumeChangeVolume is adjusted
onTimeUpdatePlayback position changes

Code Snippet

function VideoPlayer({ src }) {
  return (
    <video
      src={src}
      onPlay={() => console.log("Playing")}
      onEnded={() => console.log("Finished")}
      controls
    />
  );
}

17. Image Events đŸ–ŧī¸

Code Snippet

function Avatar({ src }) {
  function handleError(event) {
    event.target.src = "/fallback-avatar.png";
  }

  return (
    <img
      src={src}
      onLoad={() => console.log("Image loaded")}
      onError={handleError}
      alt="User avatar"
    />
  );
}

Tip

Use onError to gracefully fall back to a placeholder image when the original source fails to load.

18. Animation Events đŸŽžī¸

EventFires When
onAnimationStartA CSS animation begins
onAnimationEndA CSS animation completes
onAnimationIterationAn animation iteration completes (for looping animations)

Code Snippet

function AnimatedBox() {
  return (
    <div
      className="pulse-animation"
      onAnimationEnd={() => console.log("Animation finished")}
    />
  );
}

19. Transition Events 🌊

Code Snippet

function FadeIn() {
  return (
    <div
      className="fade-transition"
      onTransitionEnd={() => console.log("Transition complete")}
    />
  );
}

Note

onTransitionEnd fires once per CSS property being transitioned — for elements transitioning multiple properties, the handler runs multiple times.

20. Scroll Events 📜

Code Snippet

function ScrollTracker() {
  function handleScroll(event) {
    console.log("Scroll position:", event.target.scrollTop);
  }

  return (
    <div onScroll={handleScroll} style={{ overflowY: "scroll", height: "300px" }}>
      {/* Long scrollable content */}
    </div>
  );
}

Warning

Scroll events can fire very frequently. Consider debouncing or throttling scroll handlers to avoid performance issues.

21. Wheel Events 🎡

Code Snippet

function ZoomableImage() {
  function handleWheel(event) {
    console.log("Wheel delta:", event.deltaY);
  }

  return <img onWheel={handleWheel} src="/chart.png" alt="Zoomable chart" />;
}

Note

onWheel captures both mouse wheel scrolling and trackpad gestures, commonly used for zoom or custom scroll interactions.

22. Composition Events 🌏

Composition events handle text input methods for languages requiring multi-keystroke character composition, such as Chinese, Japanese, and Korean IME input.

EventFires When
onCompositionStartIME composition begins
onCompositionUpdateComposition text is updated
onCompositionEndComposition finishes

Hint

Composition events matter primarily for apps supporting IME-based languages — most English-only applications can safely ignore them.

23. Event Object đŸ“Ļ

Every event handler receives a SyntheticEvent object containing useful information about the event, including the target element, event type, and relevant data.

Code Snippet

function LogClick() {
  function handleClick(event) {
    console.log(event.type);          // "click"
    console.log(event.target);        // the DOM element clicked
    console.log(event.currentTarget); // the element the handler is attached to
    console.log(event.timeStamp);     // when the event occurred
  }

  return <button onClick={handleClick}>Click Me</button>;
}

Tip

Access the underlying native browser event via event.nativeEvent if you need APIs not exposed by the SyntheticEvent wrapper.

24. Preventing Default Behavior 🛑

Many DOM elements have default browser behaviors — like form submission reloading the page, or links navigating away. Call event.preventDefault() to stop these defaults.

Code Snippet

function CustomLink() {
  function handleClick(event) {
    event.preventDefault();
    console.log("Custom navigation logic here");
  }

  return <a href="/page" onClick={handleClick}>Custom Link</a>;
}

Important

preventDefault() stops the browser's default action, while stopPropagation() stops the event from bubbling further — they solve different problems.

25. Event Propagation 🌊

When an event occurs on an element, it doesn't just trigger that element's handler — it travels through the DOM tree in two phases: capturing (top-down) and bubbling (bottom-up).

Event Propagation
Capturing Phase
Target Phase
Bubbling Phase
Root → Target (top-down)
Event reaches the target element
Target → Root (bottom-up)

26. Event Bubbling âŦ†ī¸

By default, most events bubble — after firing on the target element, they propagate upward through each ancestor element's handlers.

Code Snippet

function App() {
  return (
    <div onClick={() => console.log("Div clicked")}>
      <button onClick={() => console.log("Button clicked")}>
        Click Me
      </button>
    </div>
  );
}
// Clicking the button logs: "Button clicked" then "Div clicked"

Note

onMouseEnter and onMouseLeave are notable exceptions — they do not bubble, unlike most other events.

27. Event Capturing âŦ‡ī¸

To handle an event during the capturing phase (before it reaches the target), append Capture to the event handler prop name.

Code Snippet

function App() {
  return (
    <div onClickCapture={() => console.log("Div (capture phase)")}>
      <button onClick={() => console.log("Button (bubble phase)")}>
        Click Me
      </button>
    </div>
  );
}
// Logs: "Div (capture phase)" then "Button (bubble phase)"

28. Stopping Propagation ✋

Call event.stopPropagation() to prevent an event from continuing to bubble (or capture) further through the DOM tree.

Code Snippet

function Modal({ onClose }) {
  function handleContentClick(event) {
    event.stopPropagation(); // prevents closing when clicking inside
  }

  return (
    <div className="overlay" onClick={onClose}>
      <div className="modal-content" onClick={handleContentClick}>
        Modal content
      </div>
    </div>
  );
}

Caution

Overusing stopPropagation() can make debugging harder, since other components relying on bubbled events (e.g., global click-outside listeners) will silently stop working.

29. Event Delegation đŸŽ¯

Event delegation attaches a single handler to a parent element instead of individual handlers on many children, relying on bubbling to catch events from descendants.

Code Snippet

function ItemList({ items, onItemClick }) {
  function handleListClick(event) {
    const itemId = event.target.dataset.id;
    if (itemId) onItemClick(itemId);
  }

  return (
    <ul onClick={handleListClick}>
      {items.map((item) => (
        <li key={item.id} data-id={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

Information

React's event system already delegates events internally at the root, so manual delegation is mainly useful for reducing handler complexity in large dynamic lists, not raw performance.

30. Custom Event Handling Patterns 🎨

Custom hooks and callback props allow you to build reusable event-handling logic that can be shared across components.

Custom Hook: useClickOutside

function useClickOutside(ref, onClickOutside) {
  useEffect(() => {
    function handleClick(event) {
      if (ref.current && !ref.current.contains(event.target)) {
        onClickOutside();
      }
    }
    document.addEventListener("mousedown", handleClick);
    return () => document.removeEventListener("mousedown", handleClick);
  }, [ref, onClickOutside]);
}

Best Practice

Extract shared event logic like this into custom Hooks to keep components clean and encourage reuse across your application.

31. Performance Considerations ⚡

  • Debounce or throttle high-frequency events like scroll, resize, and mousemove.
  • Avoid creating new inline functions on every render for performance-critical child components wrapped in memo.
  • Use useCallback to memoize handlers passed to memoized child components.
  • Prefer event delegation over attaching individual handlers to hundreds of list items.

Code Snippet

const handleClick = useCallback(() => {
  console.log("Clicked");
}, []);

32. TypeScript with Events 🔷

TS provides specific event types for different element and event combinations, offering strong autocompletion and type safety.

Code Snippet

function Input() {
  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    console.log(event.target.value);
  }

  function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
    console.log("Clicked");
  }

  return (
    <>
      <input onChange={handleChange} />
      <button onClick={handleClick}>Submit</button>
    </>
  );
}

Tip

Common event types include React.MouseEvent, React.ChangeEvent, React.KeyboardEvent, and React.FormEvent, each generic over the target element type.

33. Best Practices 🌟

  1. Name handler functions descriptively, prefixed with handle (e.g., handleSubmit).
  2. Always call preventDefault() in form submission handlers to avoid unwanted page reloads.
  3. Use arrow functions only when passing arguments; otherwise pass function references directly.
  4. Debounce or throttle handlers for high-frequency events like scrolling or resizing.
  5. Clean up manually attached document or window listeners inside useEffect cleanup functions.

34. Common Mistakes đŸšĢ

  • Calling the handler immediately: onClick={handleClick()} instead of onClick={handleClick}.
  • Forgetting event.preventDefault() in form onSubmit handlers.
  • Not cleaning up manually added event listeners, causing memory leaks.
  • Overusing stopPropagation(), breaking unrelated event-driven features elsewhere in the app.
  • Attaching heavy inline logic directly in JSX instead of extracting a named handler function.

Danger

Forgetting to remove a document.addEventListener call in a useEffect cleanup function is a common cause of memory leaks and duplicate handler execution.

35. Frequently Asked Questions ❓

Question

Are React events the same as native DOM events?

Answer

Not exactly — React wraps native events in a SyntheticEvent for cross-browser consistency, though the native event remains accessible via event.nativeEvent.

Question

Why doesn't my onMouseEnter handler fire on child elements?

Answer

onMouseEnter and onMouseLeave do not bubble by design — use onMouseOver and onMouseOut if you need bubbling behavior.

Question

How do I handle events outside of React-managed elements?

Answer

Use useEffect to manually attach and clean up listeners on document or window for events outside React's tree, such as global keyboard shortcuts.

36. Summary 📝

React's event system provides a consistent, cross-browser way to handle user interactions through SyntheticEvents, while still granting access to native browser behavior when needed. Understanding propagation, delegation, and proper cleanup patterns is essential for building responsive, bug-free interactive applications.

Summary

With event handling covered, the natural next step is exploring the Component Lifecycle and useEffect, which govern how components respond to events, state changes, and external systems over time.