React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

eventshandlerssynthetic-eventstypescript

Event Handling

Respond to user interactions with type-safe event handlers.

Recipe

Quick-reference recipe card — copy-paste ready.

// Inline handler
<button onClick={() => console.log("clicked")}>Click me</button>
 
// Named handler
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
  console.log(e.currentTarget.name);
}
<button name="save" onClick={handleClick}>Save</button>
 
// Passing data to handlers
<button onClick={() => deleteItem(item.id)}>Delete</button>
 
// Preventing default
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>

When to reach for this: Any time you need to respond to clicks, key presses, form submissions, focus changes, or any other DOM event.

Working Example

"use client";
 
import { useState, useCallback } from "react";
 
interface LogEntry {
  id: number;
  type: string;
  detail: string;
  timestamp: string;
}
 
let entryId = 0;
 
export function EventPlayground() {
  const [log, setLog] = useState<LogEntry[]>([]);
 
  const addLog = useCallback((type: string, detail: string) => {
    setLog(prev => [
      { id: ++entryId, type, detail, timestamp: new Date().toLocaleTimeString() },
      ...prev.slice(0, 9), // keep last 10
    ]);
  }, []);
 
  function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
    addLog("click", `Button "${e.currentTarget.textContent}" at (${e.clientX}, ${e.clientY})`);
  }
 
  function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
    if (e.key === "Enter") {
      addLog("keydown", `Enter pressed — value: "${e.currentTarget.value}"`);
      e.currentTarget.value = "";
    }
  }
 
  function handleFocus(e: React.FocusEvent<HTMLInputElement>) {
    addLog("focus", `Input "${e.currentTarget.placeholder}" focused`);
  }
 
  function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    addLog("submit", `Form submitted with name: "${formData.get("name")}"`);
    e.currentTarget.reset();
  }
 
  return (
    <div className="max-w-md space-y-4 rounded border p-4">
      <div className="flex gap-2">
        <button onClick={handleClick} className="rounded bg-blue-600 px-3 py-1 text-white">
          Button A
        </button>
        <button onClick={handleClick} className="rounded bg-green-600 px-3 py-1 text-white">
          Button B
        </button>
      </div>
 
      <input
        onKeyDown={handleKeyDown}
        onFocus={handleFocus}
        placeholder="Press Enter here..."
        className="w-full rounded border px-3 py-1"
      />
 
      <form onSubmit={handleSubmit} className="flex gap-2">
        <input
          name="name"
          placeholder="Your name"
          onFocus={handleFocus}
          className="flex-1 rounded border px-3 py-1"
        />
        <button type="submit" className="rounded bg-purple-600 px-3 py-1 text-white">
          Submit
        </button>
      </form>
 
      <div className="max-h-48 overflow-y-auto rounded bg-gray-50 p-2">
        <h3 className="mb-1 text-xs font-bold uppercase text-gray-500">Event Log</h3>
        {log.length === 0 && <p className="text-xs text-gray-400">Interact with the controls above...</p>}
        {log.map(entry => (
          <div key={entry.id} className="border-b py-1 text-xs">
            <span className="font-mono font-bold text-blue-700">{entry.type}</span>{" "}
            <span className="text-gray-600">{entry.detail}</span>{" "}
            <span className="text-gray-400">{entry.timestamp}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

What this demonstrates:

  • Multiple event types: onClick, onKeyDown, onFocus, onSubmit
  • Typed event objects with React.MouseEvent, React.KeyboardEvent, React.FocusEvent, React.FormEvent
  • e.preventDefault() to stop form submission from reloading the page
  • e.currentTarget to access the element the handler is attached to
  • useCallback to stabilize a frequently-called helper

Deep Dive

How It Works

  • React attaches a single event listener at the root of the tree (event delegation), not on each individual element
  • When a DOM event fires, React wraps the native event in a SyntheticEvent that normalizes cross-browser differences
  • e.currentTarget is the element the handler is attached to; e.target is the element that originated the event (may be a child)
  • React events bubble through the React tree, which may differ from the DOM tree when using portals
  • Event handlers run during the browser's event dispatch — they're synchronous and have access to the event object

Common Event Types

React PropTypeScript TypeNative Event
onClickReact.MouseEvent<HTMLElement>click
onChangeReact.ChangeEvent<HTMLInputElement>input (React fires on every keystroke)
onSubmitReact.FormEvent<HTMLFormElement>submit
onKeyDownReact.KeyboardEvent<HTMLElement>keydown
onFocusReact.FocusEvent<HTMLElement>focus
onBlurReact.FocusEvent<HTMLElement>blur
onMouseEnterReact.MouseEvent<HTMLElement>mouseenter
onPointerDownReact.PointerEvent<HTMLElement>pointerdown
onDragReact.DragEvent<HTMLElement>drag

Variations

Stop propagation:

function handleInner(e: React.MouseEvent) {
  e.stopPropagation(); // prevents outer onClick from firing
}
 
<div onClick={() => console.log("outer")}>
  <button onClick={handleInner}>Click me</button>
</div>

Capture phase:

// "Capture" variant fires top-down before the bubble phase
<div onClickCapture={() => console.log("captured first")}>
  <button onClick={() => console.log("bubbled second")}>Click</button>
</div>

Accessing native event:

function handleClick(e: React.MouseEvent) {
  const nativeEvent = e.nativeEvent; // the underlying DOM Event
  console.log(nativeEvent instanceof MouseEvent); // true
}

Custom data via closures:

function handleDelete(itemId: string) {
  setItems(prev => prev.filter(i => i.id !== itemId));
}
 
{items.map(item => (
  <button key={item.id} onClick={() => handleDelete(item.id)}>
    Delete {item.name}
  </button>
))}

TypeScript Notes

// Typing a handler prop
interface SearchBarProps {
  onSearch: (query: string) => void;
}
 
function SearchBar({ onSearch }: SearchBarProps) {
  return (
    <input
      type="search"
      onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
        onSearch(e.target.value);
      }}
    />
  );
}
 
// Inline handler type (useful for callback props)
type ClickHandler = React.MouseEventHandler<HTMLButtonElement>;
 
// Generic element type
function logEvent<T extends HTMLElement>(e: React.SyntheticEvent<T>) {
  console.log(e.currentTarget.tagName);
}

Gotchas

  • Calling the function instead of passing itonClick={handleClick()} invokes the function during render, not on click. Fix: Pass the reference: onClick={handleClick} or wrap in an arrow: onClick={() => handleClick(arg)}.

  • Stale closures in handlers — An event handler created inside a component captures the state at render time. If state updates before the handler runs (rare), the handler sees old values. Fix: Use useCallback with correct dependencies, or read from a ref for truly-latest values.

  • React onChange vs DOM change — React's onChange fires on every keystroke (like the DOM input event), not on blur like the native change event. This surprises developers coming from vanilla JS. Fix: Just know this — it's the intended behavior.

  • Passive event listeners — React does not support the { passive: true } option via JSX props. For touch/wheel handlers where you need passive: false to call preventDefault(), you may need a manual addEventListener in a useEffect. Fix: Use useEffect + ref to attach the native listener.

  • Arrow functions in renderonClick={() => doSomething(id)} creates a new function each render, which can defeat React.memo. Fix: For hot paths (large lists), extract the handler or use useCallback. For most cases, it's fine — don't optimize prematurely.

Alternatives

AlternativeUse WhenDon't Use When
useEffect + addEventListenerYou need passive listeners, window-level events, or events React doesn't wrapThe event is on a specific React element (use JSX handler)
Form Actions (React 19)Handling form submissions with server actionsClient-only forms with no server interaction
Ref-based event delegationBuilding a library that needs raw DOM event controlNormal application code

FAQs

What is a SyntheticEvent in React?

A cross-browser wrapper around the native DOM event. React creates a SyntheticEvent for every event handler, normalizing browser differences. Access the native event via e.nativeEvent if needed.

What is the difference between e.target and e.currentTarget?
  • e.currentTarget — the element the handler is attached to
  • e.target — the element that actually triggered the event (may be a child)
  • Use currentTarget when reading attributes of the element you attached the handler to
Why does onClick={handleClick()} call the function immediately?

Adding () invokes the function during render and passes its return value as the handler. Pass the reference instead: onClick={handleClick} or wrap it: onClick={() => handleClick(arg)}.

How do I pass data to an event handler?

Wrap the handler in an arrow function:

<button onClick={() => deleteItem(item.id)}>Delete</button>
How do I prevent a form from reloading the page on submit?

Call e.preventDefault() in the onSubmit handler:

<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
How do I type an event handler in TypeScript?

Use the React event types with the HTML element generic:

function handleClick(e: React.MouseEvent<HTMLButtonElement>) { ... }
function handleChange(e: React.ChangeEvent<HTMLInputElement>) { ... }
function handleSubmit(e: React.FormEvent<HTMLFormElement>) { ... }
Does React onChange fire on every keystroke?

Yes. React's onChange behaves like the native input event, firing on every keystroke — not on blur like the native change event. This is by design for real-time form updates.

How do I stop an event from bubbling to parent handlers?

Call e.stopPropagation() in the child's event handler. For the capture phase (top-down), use onClickCapture instead of onClick.

Do arrow functions in onClick cause performance problems?

In most cases, no. The inline arrow creates a new function each render, which can defeat React.memo on child components. Only optimize with useCallback in hot paths like large lists.

How do I listen for keyboard shortcuts or window-level events?

Use useEffect with addEventListener on window or document:

useEffect(() => {
  const handler = (e: KeyboardEvent) => {
    if (e.key === "Escape") closeModal();
  };
  window.addEventListener("keydown", handler);
  return () => window.removeEventListener("keydown", handler);
}, []);
How do I type a handler prop passed to a child component?

Type it as a function accepting the data you need:

interface Props {
  onSearch: (query: string) => void;
}

Or use React's built-in handler type: React.MouseEventHandler<HTMLButtonElement>.

  • Forms — form-specific event handling patterns
  • Components — passing event handlers as props
  • Refs — attaching native event listeners when React props aren't enough