Skip to main content
Advertisement

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);
};
Advertisement