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 pagee.currentTargetto access the element the handler is attached touseCallbackto 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
SyntheticEventthat normalizes cross-browser differences e.currentTargetis the element the handler is attached to;e.targetis 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 Prop | TypeScript Type | Native Event |
|---|---|---|
onClick | React.MouseEvent<HTMLElement> | click |
onChange | React.ChangeEvent<HTMLInputElement> | input (React fires on every keystroke) |
onSubmit | React.FormEvent<HTMLFormElement> | submit |
onKeyDown | React.KeyboardEvent<HTMLElement> | keydown |
onFocus | React.FocusEvent<HTMLElement> | focus |
onBlur | React.FocusEvent<HTMLElement> | blur |
onMouseEnter | React.MouseEvent<HTMLElement> | mouseenter |
onPointerDown | React.PointerEvent<HTMLElement> | pointerdown |
onDrag | React.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 it —
onClick={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
useCallbackwith correct dependencies, or read from a ref for truly-latest values. -
React
onChangevs DOMchange— React'sonChangefires on every keystroke (like the DOMinputevent), not on blur like the nativechangeevent. 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 needpassive: falseto callpreventDefault(), you may need a manualaddEventListenerin auseEffect. Fix: UseuseEffect+refto attach the native listener. -
Arrow functions in render —
onClick={() => doSomething(id)}creates a new function each render, which can defeatReact.memo. Fix: For hot paths (large lists), extract the handler or useuseCallback. For most cases, it's fine — don't optimize prematurely.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
useEffect + addEventListener | You need passive listeners, window-level events, or events React doesn't wrap | The event is on a specific React element (use JSX handler) |
| Form Actions (React 19) | Handling form submissions with server actions | Client-only forms with no server interaction |
| Ref-based event delegation | Building a library that needs raw DOM event control | Normal 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 toe.target— the element that actually triggered the event (may be a child)- Use
currentTargetwhen 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>.
Related
- Forms — form-specific event handling patterns
- Components — passing event handlers as props
- Refs — attaching native event listeners when React props aren't enough