react-syntax-events
react-syntax-events
Quick Reference
Synthetic Event System
React wraps all native browser events in SyntheticEvent objects. These wrappers normalize cross-browser differences and provide a consistent API.
| Concept | Detail |
|---|---|
| Wrapper | Every handler receives a SyntheticEvent, NOT a native Event |
| Delegation | React attaches ONE listener to the root container, NOT to individual DOM nodes |
| Pooling | React 17+ does NOT pool events -- accessing e in async callbacks is safe |
| Native access | Use e.nativeEvent to access the underlying browser event |
TypeScript Event Types
| React Type | Native Equivalent | Common Elements |
|---|---|---|
React.MouseEvent<T> |
MouseEvent |
HTMLButtonElement, HTMLDivElement |
React.ChangeEvent<T> |
Event |
HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement |
React.FormEvent<T> |
Event |
HTMLFormElement |
React.KeyboardEvent<T> |
KeyboardEvent |
HTMLInputElement, HTMLDivElement |
React.FocusEvent<T> |
FocusEvent |
HTMLInputElement, HTMLButtonElement |
React.DragEvent<T> |
DragEvent |
HTMLDivElement, HTMLImageElement |
React.TouchEvent<T> |
TouchEvent |
HTMLDivElement, HTMLButtonElement |
React.ClipboardEvent<T> |
ClipboardEvent |
HTMLInputElement, HTMLDivElement |
React.WheelEvent<T> |
WheelEvent |
HTMLDivElement, HTMLElement |
React.PointerEvent<T> |
PointerEvent |
HTMLDivElement, HTMLButtonElement |
The generic parameter T specifies the element the handler is attached to. ALWAYS provide it for correct e.currentTarget typing.
Critical Warnings
NEVER use onKeyPress -- it is deprecated. ALWAYS use onKeyDown or onKeyUp instead.
NEVER type event handlers as (e: any) => void -- ALWAYS use the specific React event type with the correct element generic.
NEVER call e.stopPropagation() as a default habit -- it breaks parent listeners, analytics tools, and modal close handlers. ONLY use it when you have a specific reason.
ALWAYS call e.preventDefault() in onSubmit handlers to prevent full page reload.
ALWAYS provide the element generic parameter (e.g., <HTMLInputElement>) -- without it, e.currentTarget is typed as HTMLElement and you lose access to element-specific properties like .value.
NEVER read e.currentTarget inside async/await or setTimeout -- currentTarget is set to null after the event handler returns. Extract the value synchronously first.
Event Handler Typing Patterns
Inline Handler
<button onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget.disabled);
}}>
Click
</button>
Extracted Handler
const handleClick = (e: React.MouseEvent<HTMLButtonElement>): void => {
console.log(e.currentTarget.disabled);
};
<button onClick={handleClick}>Click</button>
Handler as Props
interface ButtonProps {
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
onFocus?: (e: React.FocusEvent<HTMLButtonElement>) => void;
}
const Button = ({ onClick, onFocus }: ButtonProps): JSX.Element => (
<button onClick={onClick} onFocus={onFocus}>Click</button>
);
React.EventHandler Type
React provides shorthand types for handler props:
interface Props {
onClick: React.MouseEventHandler<HTMLButtonElement>;
onChange: React.ChangeEventHandler<HTMLInputElement>;
onSubmit: React.FormEventHandler<HTMLFormElement>;
onKeyDown: React.KeyboardEventHandler<HTMLInputElement>;
}
These are equivalent to (e: React.MouseEvent<HTMLButtonElement>) => void, etc.
Passing Data to Handlers
Arrow Function (Preferred)
const handleDelete = (id: string, e: React.MouseEvent<HTMLButtonElement>): void => {
e.stopPropagation();
deleteItem(id);
};
{items.map((item) => (
<button key={item.id} onClick={(e) => handleDelete(item.id, e)}>
Delete {item.name}
</button>
))}
Currying Pattern
const handleAction = (id: string) => (e: React.MouseEvent<HTMLButtonElement>): void => {
e.preventDefault();
performAction(id);
};
<button onClick={handleAction(item.id)}>Act</button>
NEVER use .bind(this, arg) in function components -- it creates a new function reference every render just like arrow functions, but with worse readability.
stopPropagation vs preventDefault
| Method | Purpose | Use When |
|---|---|---|
e.preventDefault() |
Prevents the browser default action | Form submit, link navigation, drag default behavior |
e.stopPropagation() |
Stops the event from reaching parent handlers | Nested clickable elements, preventing parent dismissals |
Decision Tree
- Is the browser doing something unwanted (page reload, navigation)? -->
preventDefault() - Is a parent handler firing when it should not? -->
stopPropagation() - Both? --> Call both explicitly
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>): void => {
e.preventDefault(); // stop navigation
e.stopPropagation(); // stop parent onClick
openModal();
};
Capture Phase
React supports capture-phase listeners by appending Capture to any event prop:
<div onClickCapture={(e: React.MouseEvent<HTMLDivElement>) => {
// Fires BEFORE child onClick handlers
console.log("Captured click on:", e.target);
}}>
<button onClick={() => console.log("Button clicked")}>
Click me
</button>
</div>
Event flow order: Capture (root to target) --> Target --> Bubble (target to root)
ALWAYS use capture-phase handlers when you need to intercept events before children process them (e.g., global escape key handling, focus trapping).
Form Events
onChange
Fires on every keystroke for text inputs (unlike native change which fires on blur):
const [value, setValue] = useState<string>("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setValue(e.currentTarget.value);
};
<input type="text" value={value} onChange={handleChange} />
onSubmit
ALWAYS call e.preventDefault() to prevent page reload:
const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
submitForm(Object.fromEntries(formData));
};
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<button type="submit">Submit</button>
</form>
Mouse Events
| Prop | Fires When | Bubbles |
|---|---|---|
onClick |
Click (mousedown + mouseup) | Yes |
onDoubleClick |
Double click | Yes |
onMouseDown / onMouseUp |
Press / release | Yes |
onMouseEnter |
Pointer enters element | No |
onMouseLeave |
Pointer leaves element | No |
onMouseOver / onMouseOut |
Pointer enters/leaves (includes children) | Yes |
onContextMenu |
Right-click | Yes |
onMouseEnter / onMouseLeave do NOT bubble -- they fire only for the exact element, not its children. Use these for hover states. Use onMouseOver / onMouseOut when you need bubbling behavior.
Keyboard Events
| Prop | Fires When |
|---|---|
onKeyDown |
Key pressed down (repeats while held) |
onKeyUp |
Key released |
NEVER use onKeyPress -- it is deprecated and does not fire for non-character keys (Escape, Arrow keys, etc.).
Key Detection
ALWAYS use e.key (string value) instead of e.keyCode (deprecated numeric code):
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
if (e.key === "Enter") {
submitSearch();
}
if (e.key === "Escape") {
clearInput();
}
};
Modifier Keys
const handleShortcut = (e: React.KeyboardEvent<HTMLDivElement>): void => {
if (e.key === "s" && (e.metaKey || e.ctrlKey)) {
e.preventDefault(); // prevent browser save dialog
saveDocument();
}
};
| Property | Key |
|---|---|
e.ctrlKey |
Ctrl (Windows/Linux) |
e.metaKey |
Cmd (macOS) / Win (Windows) |
e.shiftKey |
Shift |
e.altKey |
Alt / Option |
Focus Events
| Prop | Fires When | Bubbles |
|---|---|---|
onFocus |
Element gains focus | Yes (in React) |
onBlur |
Element loses focus | Yes (in React) |
React's onFocus and onBlur bubble, unlike the native focus/blur events. This matches the native focusin/focusout behavior.
relatedTarget
const handleBlur = (e: React.FocusEvent<HTMLInputElement>): void => {
if (e.relatedTarget === null) {
// Focus left the document entirely
validateField();
}
};
e.relatedTarget is the element that received (onBlur) or lost (onFocus) focus. It is null when focus moves outside the document.
React 18 vs React 19 Differences
| Feature | React 18 | React 19 |
|---|---|---|
| Event delegation | Root container | Root container (unchanged) |
| Event pooling | Removed (since 17) | Removed |
ref as prop |
Use forwardRef for function components |
ref is a regular prop -- no forwardRef needed |
| Form actions | Not available | <form action={fn}> with useActionState |
In React 19, forms can use the action prop directly. For new projects on React 19, prefer action + useActionState over manual onSubmit + preventDefault for form submissions with server mutations.
Reference Links
- references/examples.md -- Complete event handling examples with TypeScript
- references/api-table.md -- All synthetic event types with properties and TypeScript signatures
- references/anti-patterns.md -- Common event handling mistakes and corrections