Typing Events
Recipe
Type React event handlers correctly for clicks, form submissions, input changes, keyboard events, and custom event callbacks. Use React's synthetic event types for full type safety.
Working Example
// Click event
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
console.log("Button clicked at", event.clientX, event.clientY);
}
// Form submit event
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const email = formData.get("email") as string;
console.log("Submitted:", email);
}
// Input change event
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
console.log("Value:", event.target.value);
}
// Keyboard event
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (event.key === "Enter") {
console.log("Enter pressed");
}
}// Full form component
function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Escape") setEmail("");
}}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Log In</button>
</form>
);
}Deep Dive
How It Works
- React wraps native DOM events in
SyntheticEventobjects. TypeScript provides generic event types likeReact.MouseEvent<T>whereTis the HTML element type. event.targetis typed asEventTarget, which is broad. Useevent.currentTargetfor the element the handler is attached to -- it is typed as the generic parameter (HTMLButtonElement,HTMLInputElement, etc.).- When writing inline handlers (
onChange={(e) => ...}), TypeScript infers the event type automatically from the JSX attribute. - When extracting handlers to named functions, you must annotate the event parameter explicitly.
Variations
Select and textarea:
function handleSelectChange(event: React.ChangeEvent<HTMLSelectElement>) {
console.log("Selected:", event.target.value);
}
function handleTextareaChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
console.log("Text:", event.target.value);
}Drag events:
function handleDragStart(event: React.DragEvent<HTMLDivElement>) {
event.dataTransfer.setData("text/plain", "dragged");
}Focus events:
function handleFocus(event: React.FocusEvent<HTMLInputElement>) {
event.currentTarget.select();
}Custom event callback props:
type SearchBarProps = {
onSearch: (query: string) => void;
onClear?: () => void;
};
function SearchBar({ onSearch, onClear }: SearchBarProps) {
const [query, setQuery] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSearch(query);
};
return (
<form onSubmit={handleSubmit}>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<button type="submit">Search</button>
{onClear && <button type="button" onClick={onClear}>Clear</button>}
</form>
);
}TypeScript Notes
- Common event types:
React.MouseEvent,React.ChangeEvent,React.FormEvent,React.KeyboardEvent,React.FocusEvent,React.DragEvent,React.TouchEvent,React.WheelEvent. - The generic parameter determines the type of
currentTarget. For example,React.MouseEvent<HTMLAnchorElement>givescurrentTargetthe typeHTMLAnchorElement. React.SyntheticEventis the base type for all React events. Use it when you do not care about the specific event type.
Gotchas
event.targetvsevent.currentTarget:targetis the element that triggered the event (could be a child), whilecurrentTargetis the element the handler is attached to. TypeScript typescurrentTargetmore precisely.- Using
event.target.valueon a form submit event will error becauseEventTargetdoes not have avalueproperty. Cast it or usecurrentTarget. - React's synthetic events are pooled in React 16 and earlier. In React 17+, event pooling is removed, so you can safely access event properties asynchronously.
- Forgetting
event.preventDefault()on form submit causes a full page reload.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
| Inline arrow functions | TypeScript infers event type automatically | Creates new function on each render |
| Named handler functions | Reusable, testable, no re-creation | Must manually annotate event type |
React.EventHandler<E> type | Concise handler type alias | Less common, slightly harder to read |
Callback prop (e.g., onSearch: (q: string) => void) | Decouples parent from DOM events | Parent does not have access to raw event |
useCallback wrapped handler | Stable reference for memoized children | Additional boilerplate |
FAQs
What is the difference between event.target and event.currentTarget?
event.targetis the element that triggered the event (could be a child element).event.currentTargetis the element the handler is attached to.- TypeScript types
currentTargetmore precisely based on the generic parameter.
Why does TypeScript infer event types automatically for inline handlers but not named functions?
- Inline handlers like
onChange={(e) => ...}get their type from the JSX attribute context. - Named functions exist outside that context, so TypeScript cannot infer the event type.
- You must annotate the event parameter explicitly:
function handleChange(e: React.ChangeEvent<HTMLInputElement>).
What are the most common React event types?
React.MouseEvent<T>-- clicks, mouse movement.React.ChangeEvent<T>-- input, select, textarea changes.React.FormEvent<T>-- form submissions.React.KeyboardEvent<T>-- key presses.React.FocusEvent<T>-- focus and blur.
Gotcha: Why does accessing event.target.value on a form submit event cause a TypeScript error?
event.targetis typed asEventTarget, which does not have avalueproperty.- Use
event.currentTarget(typed as the form element) or castevent.target. - For form data, prefer
new FormData(event.currentTarget).
How do you type a custom event callback prop?
type SearchBarProps = {
onSearch: (query: string) => void;
onClear?: () => void;
};- Expose a domain-specific callback (e.g.,
onSearch(query)) instead of the raw DOM event. - This decouples the parent from the child's DOM implementation.
What does the generic parameter in React.MouseEvent<HTMLButtonElement> control?
- It determines the type of
event.currentTarget. React.MouseEvent<HTMLButtonElement>meanscurrentTargetis typed asHTMLButtonElement.- Use the HTML element type that matches the element the handler is attached to.
Gotcha: What happens if you forget event.preventDefault() on form submit?
- The browser performs a full page reload (default form submission behavior).
- Your SPA state is lost and the form handler's logic may not complete.
- Always call
event.preventDefault()at the top of form submit handlers.
When should you use React.SyntheticEvent as the event type?
- When you do not care about the specific event type (mouse, keyboard, etc.).
- It is the base type for all React events.
- Prefer specific types when you need access to event-specific properties (e.g.,
clientX,key).
How do you type a handler for a select element?
function handleSelectChange(event: React.ChangeEvent<HTMLSelectElement>) {
console.log("Selected:", event.target.value);
}- Use
React.ChangeEvent<HTMLSelectElement>-- notHTMLInputElement. - The
valueproperty is the string value of the selected option.
Is event pooling still a concern in React 17+?
- No. React 17 removed event pooling entirely.
- In React 16 and earlier, synthetic events were recycled, and accessing properties asynchronously required
event.persist(). - In React 17+, you can safely access event properties in async callbacks without
persist().