10.3 Event Handler Types — SyntheticEvent and React Events
React's Synthetic Events (SyntheticEvent)
React uses SyntheticEvent which wraps native browser events. You need to specify event types precisely in TypeScript to get correct auto-completion.
import { SyntheticEvent, ChangeEvent, MouseEvent, FormEvent } from 'react';
Common Event Types
ChangeEvent — Input Value Changes
// ChangeEvent<HTMLElement>: used with input, select, textarea
function InputExample() {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value); // string
console.log(e.target.checked); // boolean (for checkbox)
console.log(e.target.files); // FileList | null (for file input)
};
const handleSelectChange = (e: ChangeEvent<HTMLSelectElement>) => {
console.log(e.target.value); // selected option's value
console.log(e.target.selectedOptions); // HTMLCollectionOf<HTMLOptionElement>
};
const handleTextareaChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
console.log(e.target.value);
};
return (
<>
<input onChange={handleChange} />
<select onChange={handleSelectChange}>
<option value="a">A</option>
<option value="b">B</option>
</select>
<textarea onChange={handleTextareaChange} />
</>
);
}
MouseEvent — Mouse Events
function MouseExample() {
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
console.log(e.clientX, e.clientY); // Mouse position
console.log(e.button); // 0: left, 1: middle, 2: right
console.log(e.shiftKey); // Whether Shift key is pressed
console.log(e.currentTarget); // HTMLButtonElement
};
const handleDivClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation(); // Stop event bubbling
e.preventDefault(); // Prevent default behavior
};
return (
<div onClick={handleDivClick}>
<button onClick={handleClick}>Click</button>
</div>
);
}
FormEvent — Form Submission
interface LoginForm {
email: string;
password: string;
}
function LoginForm() {
const [form, setForm] = useState<LoginForm>({ email: '', password: '' });
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); // Prevent default form submission
const formData = new FormData(e.currentTarget);
const email = formData.get('email') as string;
const password = formData.get('password') as string;
await login({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
name="email"
type="email"
value={form.email}
onChange={e => setForm(prev => ({ ...prev, email: e.target.value }))}
/>
<input
name="password"
type="password"
value={form.password}
onChange={e => setForm(prev => ({ ...prev, password: e.target.value }))}
/>
<button type="submit">Login</button>
</form>
);
}
Common Event Types Reference
import {
// Mouse
MouseEvent,
MouseEventHandler, // Alias for (e: MouseEvent) => void
// Keyboard
KeyboardEvent,
KeyboardEventHandler,
// Input
ChangeEvent,
ChangeEventHandler,
// Form
FormEvent,
FormEventHandler,
// Focus
FocusEvent,
FocusEventHandler,
// Drag
DragEvent,
DragEventHandler,
// Touch
TouchEvent,
TouchEventHandler,
// Wheel
WheelEvent,
WheelEventHandler,
// Clipboard
ClipboardEvent,
ClipboardEventHandler,
// Scroll
UIEvent,
} from 'react';
Passing Event Handlers as Props
// Include event handler types in Props
interface ButtonProps {
label: string;
onClick?: MouseEventHandler<HTMLButtonElement>;
onMouseEnter?: MouseEventHandler<HTMLButtonElement>;
}
// Or more specifically
interface InputProps {
value: string;
onChange: ChangeEventHandler<HTMLInputElement>;
onBlur?: FocusEventHandler<HTMLInputElement>;
onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
}
function Input({ value, onChange, onBlur, onKeyDown }: InputProps) {
return (
<input
value={value}
onChange={onChange}
onBlur={onBlur}
onKeyDown={onKeyDown}
/>
);
}
Practical Example: Complete Form Component
import { useState, ChangeEvent, FormEvent, FocusEvent } from 'react';
interface Field {
value: string;
error: string;
touched: boolean;
}
interface FormState {
name: Field;
email: Field;
message: Field;
}
type FieldName = keyof FormState;
function ContactForm() {
const [form, setForm] = useState<FormState>({
name: { value: '', error: '', touched: false },
email: { value: '', error: '', touched: false },
message: { value: '', error: '', touched: false },
});
const validate = (name: FieldName, value: string): string => {
switch (name) {
case 'name':
return value.length < 2 ? 'Name must be at least 2 characters' : '';
case 'email':
return !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
? 'Please enter a valid email address'
: '';
case 'message':
return value.length < 10 ? 'Message must be at least 10 characters' : '';
default:
return '';
}
};
const handleChange = (name: FieldName) =>
(e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { value } = e.target;
setForm(prev => ({
...prev,
[name]: {
...prev[name],
value,
error: prev[name].touched ? validate(name, value) : '',
},
}));
};
const handleBlur = (name: FieldName) =>
(_: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setForm(prev => ({
...prev,
[name]: {
...prev[name],
touched: true,
error: validate(name, prev[name].value),
},
}));
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Full validation
const hasErrors = Object.entries(form).some(
([name, field]) => validate(name as FieldName, field.value) !== ''
);
if (hasErrors) return;
console.log('Submitted:', {
name: form.name.value,
email: form.email.value,
message: form.message.value,
});
};
return (
<form onSubmit={handleSubmit}>
{(['name', 'email'] as const).map(field => (
<div key={field}>
<input
value={form[field].value}
onChange={handleChange(field)}
onBlur={handleBlur(field)}
placeholder={field === 'name' ? 'Name' : 'Email'}
/>
{form[field].touched && form[field].error && (
<span className="error">{form[field].error}</span>
)}
</div>
))}
<textarea
value={form.message.value}
onChange={handleChange('message')}
onBlur={handleBlur('message')}
placeholder="Message"
/>
{form.message.touched && form.message.error && (
<span className="error">{form.message.error}</span>
)}
<button type="submit">Send</button>
</form>
);
}
File Upload Events
function FileUpload() {
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
const file = files[0];
console.log(file.name); // string
console.log(file.size); // number (bytes)
console.log(file.type); // string (MIME type)
const reader = new FileReader();
reader.onload = (event) => {
const result = event.target?.result; // string | ArrayBuffer | null
console.log(result);
};
reader.readAsDataURL(file);
};
return (
<input
type="file"
accept="image/*"
onChange={handleFileChange}
/>
);
}
Pro Tips
1. Event Handler Factory Pattern
// Generic handler for multiple fields
function useFormHandlers<T extends Record<string, string>>(
setForm: React.Dispatch<React.SetStateAction<T>>
) {
const handleChange = (field: keyof T) =>
(e: ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
setForm(prev => ({ ...prev, [field]: e.target.value }));
};
return { handleChange };
}
2. Event Target Type Casting
// When you're certain the event target is a specific element
const handleClick = (e: MouseEvent) => {
const target = e.target as HTMLButtonElement;
console.log(target.dataset.id);
};