Event Handling — SyntheticEvent and Forms
The SyntheticEvent System
React wraps browser native events with a SyntheticEvent. SyntheticEvents are based on the W3C specification, behave consistently across all browsers, and provide the same interface as native events.
function ClickExample() {
function handleClick(event) {
// SyntheticEvent object
console.log(event.type); // 'click'
console.log(event.target); // The clicked DOM element
console.log(event.currentTarget); // The element the handler is bound to
console.log(event.nativeEvent); // The browser's native event
event.preventDefault(); // Prevent default behavior
event.stopPropagation(); // Stop event propagation
}
return <button onClick={handleClick}>Click</button>;
}
Event Delegation Change in React 17+
Up to React 16, all events were delegated to document. From React 17 onward, they are delegated to the React root container. This means multiple React apps can be mounted on one page without event conflicts.
Event Handler Patterns
Inline Handler
function App() {
return (
<button onClick={() => console.log('Clicked')}>
Click
</button>
);
}
Separate Function Definition
function App() {
function handleClick() {
console.log('Clicked');
}
return <button onClick={handleClick}>Click</button>;
// ✅ onClick={handleClick} — passes a function reference
// ❌ onClick={handleClick()} — calls immediately (function invocation)
}
Passing Parameters
function ProductList({ products }) {
function handleDelete(id, event) {
event.stopPropagation();
console.log(`Deleting product ${id}`);
}
return (
<ul>
{products.map(product => (
<li key={product.id} onClick={() => console.log('Item clicked')}>
{product.name}
<button onClick={(e) => handleDelete(product.id, e)}>Delete</button>
</li>
))}
</ul>
);
}
Common Events
function EventExamples() {
return (
<div
// Mouse events
onClick={() => {}}
onDoubleClick={() => {}}
onMouseEnter={() => {}}
onMouseLeave={() => {}}
onMouseMove={(e) => console.log(e.clientX, e.clientY)}
// Drag events
onDragStart={() => {}}
onDrop={() => {}}
>
<input
// Form events
onChange={(e) => console.log(e.target.value)}
onFocus={() => {}}
onBlur={() => {}}
onKeyDown={(e) => {}}
onKeyUp={(e) => {}}
/>
<form
onSubmit={(e) => e.preventDefault()}
>
</form>
</div>
);
}
Controlled Component
A pattern where React state becomes the "single source of truth" for the form.
import { useState } from 'react';
function LoginForm() {
const [formData, setFormData] = useState({
email: '',
password: '',
});
const [errors, setErrors] = useState({});
function handleChange(e) {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
// Clear the error for this field when typing
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
}
function validate() {
const newErrors = {};
if (!formData.email.includes('@')) {
newErrors.email = 'Please enter a valid email.';
}
if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters.';
}
return newErrors;
}
function handleSubmit(e) {
e.preventDefault();
const newErrors = validate();
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
console.log('Login attempt:', formData.email);
// API call...
}
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
<button type="submit">Log In</button>
</form>
);
}
Uncontrolled Component
The DOM manages the form data, and React reads the values only when needed.
import { useRef } from 'react';
function FileUploadForm() {
const fileRef = useRef(null);
const titleRef = useRef(null);
function handleSubmit(e) {
e.preventDefault();
const file = fileRef.current.files[0];
const title = titleRef.current.value;
console.log('File:', file.name, 'Title:', title);
}
return (
<form onSubmit={handleSubmit}>
<input ref={titleRef} type="text" placeholder="File description" />
<input ref={fileRef} type="file" accept="image/*" />
<button type="submit">Upload</button>
</form>
);
}
Controlled vs Uncontrolled Comparison
| Feature | Controlled Component | Uncontrolled Component |
|---|---|---|
| Data location | React state | DOM |
| Instant value access | ✅ Anytime | ❌ Only on event |
| Real-time validation | ✅ Easy | ❌ Complex |
| File input | ❌ Not possible | ✅ Required |
| Performance | Re-renders occur | No re-renders |
Keyboard Events
function SearchInput({ onSearch }) {
const [query, setQuery] = useState('');
function handleKeyDown(e) {
switch (e.key) {
case 'Enter':
onSearch(query);
break;
case 'Escape':
setQuery('');
break;
case 'ArrowUp':
e.preventDefault();
// Navigate to previous search term
break;
}
}
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Search (Enter to search, Esc to clear)"
/>
);
}
Controlling Event Bubbling
function Modal({ onClose, children }) {
return (
<div
className="backdrop"
onClick={onClose} // Close when backdrop is clicked
>
<div
className="modal-content"
onClick={e => e.stopPropagation()} // Stop bubbling when modal content is clicked
>
{children}
</div>
</div>
);
}
Practical Example: Multi-step Form
import { useState } from 'react';
const STEPS = ['Basic Info', 'Address', 'Payment'];
function MultiStepForm() {
const [step, setStep] = useState(0);
const [formData, setFormData] = useState({
name: '', email: '', phone: '',
address: '', city: '', zipCode: '',
cardNumber: '', cardExpiry: '',
});
function handleChange(e) {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
}
function handleNext(e) {
e.preventDefault();
setStep(prev => Math.min(prev + 1, STEPS.length - 1));
}
function handlePrev() {
setStep(prev => Math.max(prev - 1, 0));
}
function handleSubmit(e) {
e.preventDefault();
console.log('Final submission:', formData);
}
return (
<div className="multistep-form">
{/* Progress indicator */}
<div className="steps">
{STEPS.map((label, i) => (
<span key={i} className={i <= step ? 'active' : ''}>
{i + 1}. {label}
</span>
))}
</div>
<form onSubmit={step === STEPS.length - 1 ? handleSubmit : handleNext}>
{/* Step 0: Basic Info */}
{step === 0 && (
<div>
<h2>Basic Info</h2>
<input name="name" value={formData.name} onChange={handleChange}
placeholder="Name" required />
<input name="email" type="email" value={formData.email} onChange={handleChange}
placeholder="Email" required />
<input name="phone" value={formData.phone} onChange={handleChange}
placeholder="Phone number" />
</div>
)}
{/* Step 1: Address */}
{step === 1 && (
<div>
<h2>Shipping Address</h2>
<input name="address" value={formData.address} onChange={handleChange}
placeholder="Street address" required />
<input name="city" value={formData.city} onChange={handleChange}
placeholder="City" required />
<input name="zipCode" value={formData.zipCode} onChange={handleChange}
placeholder="ZIP code" />
</div>
)}
{/* Step 2: Payment */}
{step === 2 && (
<div>
<h2>Payment Info</h2>
<input name="cardNumber" value={formData.cardNumber} onChange={handleChange}
placeholder="Card number" required maxLength={19} />
<input name="cardExpiry" value={formData.cardExpiry} onChange={handleChange}
placeholder="MM/YY" required maxLength={5} />
</div>
)}
<div className="form-actions">
{step > 0 && (
<button type="button" onClick={handlePrev}>Previous</button>
)}
<button type="submit">
{step === STEPS.length - 1 ? 'Place Order' : 'Next'}
</button>
</div>
</form>
</div>
);
}
Pro Tips
1. Adopting React Hook Form
In production, using react-hook-form or formik solves both the performance issues of controlled components and the validation logic at once.
import { useForm } from 'react-hook-form';
function RegisterForm() {
const { register, handleSubmit, formState: { errors } } = useForm();
function onSubmit(data) {
console.log(data);
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('email', {
required: 'Email is required',
pattern: { value: /\S+@\S+/, message: 'Invalid email format' },
})}
/>
{errors.email && <span>{errors.email.message}</span>}
<button type="submit">Register</button>
</form>
);
}
2. Debouncing Event Handlers
import { useCallback } from 'react';
function useDebounce(fn, delay) {
const timerRef = useRef(null);
return useCallback((...args) => {
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => fn(...args), delay);
}, [fn, delay]);
}
function SearchInput({ onSearch }) {
const debouncedSearch = useDebounce(onSearch, 300);
return (
<input
onChange={e => debouncedSearch(e.target.value)}
placeholder="Search fires 300ms after typing stops"
/>
);
}