Event Handling
Respond to user interactions with type-safe event handlers.
Search across all documentation pages
Respond to user interactions with type-safe event handlers.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
"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:
onClick, onKeyDown, onFocus, onSubmitReact.MouseEvent, React.KeyboardEvent, React.FocusEvent, React.FormEvente.preventDefault() to stop form submission from reloading the pagee.currentTarget to access the element the handler is attached touseCallback to stabilize a frequently-called helperSyntheticEvent that normalizes cross-browser differencese.currentTarget is the element the handler is attached to; e.target is the element that originated the event (may be a child)| 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 |
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>
))}// 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);
}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 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 render - onClick={() => 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.
| 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 |
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.
e.currentTarget - the element the handler is attached toe.target - the element that actually triggered the event (may be a child)currentTarget when reading attributes of the element you attached the handler toAdding () 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)}.
Wrap the handler in an arrow function:
<button onClick={() => deleteItem(item.id)}>Delete</button>Call e.preventDefault() in the onSubmit handler:
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>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>) { ... }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.
Call e.stopPropagation() in the child's event handler. For the capture phase (top-down), use onClickCapture instead of onClick.
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.
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);
}, []);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>.
Reviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥