⚡ Events in React
Components that just display data are nice, but components that respond to user actions? That's where the magic happens! Events are how your React components come alive - responding to clicks, tracking input changes, handling form submissions, and creating truly interactive experiences. In this lesson, we'll master event handling in React with TypeScript, learning patterns that will make your apps feel responsive and polished. Let's make things interactive! 🎮
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Handle common user events (click, change, submit, etc.)
- Understand React's synthetic event system
- Type event handlers properly with TypeScript
- Pass parameters to event handlers
- Work with form inputs and controlled components
- Implement form validation and error handling
- Handle keyboard and focus events
- Prevent default behaviors and stop event propagation
Estimated Time: 70-85 minutes
Project: Build a complete interactive form with validation and dynamic feedback
📑 In This Lesson
🎯 Introduction to Events
Events are actions that happen in your application - a user clicks a button, types in a field, submits a form, or moves their mouse. React makes handling these events straightforward and consistent across browsers.
📖 Definition
Event Handler: A function that runs in response to a user action. In React, you attach event handlers directly to JSX elements using props like onClick, onChange, etc.
Common React Events
React supports all standard DOM events, organized into categories:
| Category | Events | Common Use Cases |
|---|---|---|
| Mouse | onClick, onDoubleClick, onMouseEnter, onMouseLeave | Buttons, clickable areas, hover effects |
| Keyboard | onKeyDown, onKeyUp, onKeyPress | Shortcuts, search inputs, text editing |
| Form | onChange, onSubmit, onFocus, onBlur | Inputs, forms, validation |
| Touch | onTouchStart, onTouchMove, onTouchEnd | Mobile interactions, gestures |
| Focus | onFocus, onBlur | Form validation, accessibility |
| Clipboard | onCopy, onCut, onPaste | Copy protection, formatted paste |
React Events vs DOM Events
React events work similarly to HTML events but with some key differences:
<!-- HTML/Vanilla JavaScript -->
<button onclick="handleClick()">Click me</button>
<script>
function handleClick() {
console.log('Clicked!');
}
</script>
// React/TypeScript
function MyComponent() {
const handleClick = () => {
console.log('Clicked!');
};
return <button onClick={handleClick}>Click me</button>;
}
💡 Key Differences
- Naming: camelCase in React (
onClick), lowercase in HTML (onclick) - Value: Function reference in React, string in HTML
- Preventing default: Call
e.preventDefault()in React, returnfalsein HTML - Consistency: React normalizes events across browsers
🖱️ Basic Event Handling
Let's start with the most common event - click handling!
Simple Click Handler
The most basic event handler:
function ClickExample() {
const handleClick = () => {
console.log('Button was clicked!');
};
return (
<button onClick={handleClick}>
Click me
</button>
);
}
Inline Event Handlers
For simple actions, you can write handlers inline:
function InlineExample() {
return (
<div>
{/* Inline arrow function */}
<button onClick={() => console.log('Clicked!')}>
Click me
</button>
{/* Inline with multiple statements */}
<button onClick={() => {
console.log('Starting...');
console.log('Done!');
}}>
Multi-line
</button>
</div>
);
}
⚠️ Performance Note
Inline functions are recreated on every render. For most cases this is fine, but for performance-critical components or long lists, define handlers outside the JSX.
Event Handler with State
Handlers typically update state to trigger re-renders:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(count + 1);
};
const handleDecrement = () => {
setCount(count - 1);
};
const handleReset = () => {
setCount(0);
};
return (
<div>
<h2>Count: {count}</h2>
<button onClick={handleIncrement}>+1</button>
<button onClick={handleDecrement}>-1</button>
<button onClick={handleReset}>Reset</button>
</div>
);
}
Multiple Event Types
Elements can respond to multiple events:
function MultiEventButton() {
const handleClick = () => {
console.log('Clicked!');
};
const handleMouseEnter = () => {
console.log('Mouse entered!');
};
const handleMouseLeave = () => {
console.log('Mouse left!');
};
return (
<button
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
Hover and Click me
</button>
);
}
Hover Effects with State
Track hover state for interactive UI:
function HoverCard() {
const [isHovered, setIsHovered] = useState(false);
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{
padding: '2rem',
backgroundColor: isHovered ? '#667eea' : '#f0f0f0',
color: isHovered ? 'white' : '#333',
transition: 'all 0.3s',
cursor: 'pointer'
}}
>
{isHovered ? 'Thanks for hovering!' : 'Hover over me!'}
</div>
);
}
Disabling Event Handlers
Control when handlers can be triggered:
function SubmitButton() {
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async () => {
setIsLoading(true);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
setIsLoading(false);
console.log('Submitted!');
};
return (
<button
onClick={handleSubmit}
disabled={isLoading}
>
{isLoading ? 'Submitting...' : 'Submit'}
</button>
);
}
🔄 Synthetic Events
React doesn't use native browser events directly. Instead, it wraps them in "synthetic events" for consistency and performance.
📖 Definition
Synthetic Event: React's cross-browser wrapper around the browser's native event. It has the same interface as native events but works identically across all browsers.
Why Synthetic Events?
React's event system provides several benefits:
- Cross-browser compatibility - Works the same in all browsers
- Performance - Event pooling and delegation for efficiency
- Consistency - Predictable behavior across event types
- Integration - Works seamlessly with React's rendering
Accessing Event Properties
Synthetic events have all the properties you'd expect:
function EventDetails() {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log('Event type:', event.type); // "click"
console.log('Target element:', event.target); // The button
console.log('Current target:', event.currentTarget); // Also the button
console.log('Mouse X:', event.clientX); // X coordinate
console.log('Mouse Y:', event.clientY); // Y coordinate
console.log('Button clicked:', event.button); // 0 = left, 1 = middle, 2 = right
console.log('Ctrl key held?', event.ctrlKey); // Boolean
console.log('Timestamp:', event.timeStamp); // When it happened
};
return <button onClick={handleClick}>Click for details</button>;
}
preventDefault()
Prevent the browser's default behavior:
function LinkExample() {
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault(); // Don't navigate!
console.log('Link clicked, but not navigating');
};
return (
<a href="https://example.com" onClick={handleClick}>
Click me (won't navigate)
</a>
);
}
function FormExample() {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); // Don't reload page!
console.log('Form submitted, but page not reloaded');
};
return (
<form onSubmit={handleSubmit}>
<input type="text" />
<button type="submit">Submit</button>
</form>
);
}
stopPropagation()
Stop events from bubbling up to parent elements:
function BubblingExample() {
const handleParentClick = () => {
console.log('Parent clicked');
};
const handleChildClick = (event: React.MouseEvent) => {
event.stopPropagation(); // Don't trigger parent handler
console.log('Child clicked');
};
return (
<div onClick={handleParentClick} style={{ padding: '2rem', background: '#f0f0f0' }}>
Parent (click me)
<button onClick={handleChildClick}>
Child (click me - won't trigger parent)
</button>
</div>
);
}
💡 Event Bubbling Visualization
When you click a child element, the event "bubbles" up through parent elements:
Child → Parent → Grandparent → ... → Document
Use stopPropagation() to prevent this bubbling.
Event Pooling (Note for React 16 and earlier)
In React 16 and earlier, synthetic events were pooled for performance. In React 17+, this was removed, so you can safely access event properties asynchronously:
// React 17+ - This works fine
function ModernComponent() {
const handleClick = (event: React.MouseEvent) => {
setTimeout(() => {
console.log(event.type); // ✅ Works in React 17+
}, 1000);
};
return <button onClick={handleClick}>Click me</button>;
}
🔷 TypeScript Event Types
TypeScript provides specific types for each kind of event, ensuring type safety and great autocomplete!
Common Event Types
Here are the most frequently used event types:
| Event | TypeScript Type | Example Element |
|---|---|---|
| onClick | React.MouseEvent<HTMLButtonElement> |
button, div, any element |
| onChange | React.ChangeEvent<HTMLInputElement> |
input, textarea, select |
| onSubmit | React.FormEvent<HTMLFormElement> |
form |
| onKeyDown | React.KeyboardEvent<HTMLInputElement> |
input, any focusable element |
| onFocus/onBlur | React.FocusEvent<HTMLInputElement> |
input, button, any focusable |
Click Events
// Button click
const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log('Button clicked at:', event.clientX, event.clientY);
};
// Div click (any element)
const handleDivClick = (event: React.MouseEvent<HTMLDivElement>) => {
console.log('Div clicked');
};
// Generic click (works with any element)
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
console.log('Element clicked');
};
Input Change Events
// Text input
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
console.log('Input value:', value);
};
// Textarea
const handleTextareaChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = event.target.value;
console.log('Textarea value:', value);
};
// Select dropdown
const handleSelectChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const value = event.target.value;
console.log('Selected:', value);
};
Form Submit Events
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// Get form data
const formData = new FormData(event.currentTarget);
const email = formData.get('email');
console.log('Form submitted with email:', email);
};
Keyboard Events
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
console.log('Key pressed:', event.key);
console.log('Key code:', event.code);
console.log('Ctrl held?', event.ctrlKey);
console.log('Shift held?', event.shiftKey);
console.log('Alt held?', event.altKey);
// Check for Enter key
if (event.key === 'Enter') {
console.log('Enter was pressed!');
}
// Check for Escape
if (event.key === 'Escape') {
console.log('Escape was pressed!');
}
};
Generic Event Handler Type
For functions that handle multiple event types:
// Generic handler function type
type EventHandler<T = HTMLElement> = (
event: React.MouseEvent<T> | React.KeyboardEvent<T>
) => void;
// Usage
const handleInteraction: EventHandler<HTMLButtonElement> = (event) => {
if ('key' in event) {
// It's a keyboard event
console.log('Key:', event.key);
} else {
// It's a mouse event
console.log('Mouse:', event.clientX, event.clientY);
}
};
✅ TypeScript Benefits
- Autocomplete - Your IDE suggests available event properties
- Type checking - Catch errors before runtime
- Documentation - Types show what properties are available
- Refactoring - Safely change event handling code
🎯 Choosing the Right Event Type
Use this visual guide to select the appropriate TypeScript event type:
📝 Form Events and Inputs
Forms are the backbone of user interaction on the web. React provides powerful patterns for handling form inputs through controlled components - where React state is the "single source of truth" for input values.
Controlled vs Uncontrolled Components
Text Input Handling
The most common form element. Here's how to handle text inputs with proper TypeScript typing:
import { useState } from 'react';
function TextInputExample() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
};
const handleEmailChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setEmail(event.target.value);
};
return (
<form>
<div>
<label htmlFor="name">Name:</label>
<input
id="name"
type="text"
value={name}
onChange={handleNameChange}
placeholder="Enter your name"
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
value={email}
onChange={handleEmailChange}
placeholder="Enter your email"
/>
</div>
<p>Hello, {name || 'stranger'}! Your email is {email || 'not set'}.</p>
</form>
);
}
Checkbox and Radio Inputs
Checkboxes and radio buttons use checked instead of value:
function CheckboxRadioExample() {
const [isSubscribed, setIsSubscribed] = useState(false);
const [contactMethod, setContactMethod] = useState('email');
const [interests, setInterests] = useState<string[]>([]);
// Single checkbox
const handleSubscribeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setIsSubscribed(e.target.checked);
};
// Radio buttons
const handleContactChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setContactMethod(e.target.value);
};
// Multiple checkboxes
const handleInterestChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value, checked } = e.target;
setInterests(prev =>
checked
? [...prev, value]
: prev.filter(interest => interest !== value)
);
};
return (
<form>
{/* Single Checkbox */}
<label>
<input
type="checkbox"
checked={isSubscribed}
onChange={handleSubscribeChange}
/>
Subscribe to newsletter
</label>
{/* Radio Buttons */}
<fieldset>
<legend>Preferred contact method:</legend>
<label>
<input
type="radio"
value="email"
checked={contactMethod === 'email'}
onChange={handleContactChange}
/>
Email
</label>
<label>
<input
type="radio"
value="phone"
checked={contactMethod === 'phone'}
onChange={handleContactChange}
/>
Phone
</label>
</fieldset>
{/* Multiple Checkboxes */}
<fieldset>
<legend>Interests:</legend>
{['React', 'TypeScript', 'Node.js'].map(tech => (
<label key={tech}>
<input
type="checkbox"
value={tech}
checked={interests.includes(tech)}
onChange={handleInterestChange}
/>
{tech}
</label>
))}
</fieldset>
</form>
);
}
Select Dropdowns
Select elements work similarly to text inputs:
function SelectExample() {
const [country, setCountry] = useState('');
const [languages, setLanguages] = useState<string[]>([]);
// Single select
const handleCountryChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setCountry(e.target.value);
};
// Multiple select
const handleLanguagesChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const options = e.target.options;
const selected: string[] = [];
for (let i = 0; i < options.length; i++) {
if (options[i].selected) {
selected.push(options[i].value);
}
}
setLanguages(selected);
};
return (
<form>
{/* Single Select */}
<label>
Country:
<select value={country} onChange={handleCountryChange}>
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
</select>
</label>
{/* Multiple Select */}
<label>
Languages (hold Ctrl/Cmd to select multiple):
<select
multiple
value={languages}
onChange={handleLanguagesChange}
style={{ height: '100px' }}
>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
<option value="de">German</option>
</select>
</label>
</form>
);
}
🎮 Interactive Demo: Form State Flow
This visualization shows how controlled components maintain state:
Animated visualization of controlled component data flow
✅ Best Practice: Generic Input Handler
Instead of creating separate handlers for each input, use a single handler with the input's name attribute:
function GenericFormExample() {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
age: ''
});
// Single handler for all inputs!
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value // Computed property name
}));
};
return (
<form>
<input name="firstName" value={formData.firstName} onChange={handleChange} />
<input name="lastName" value={formData.lastName} onChange={handleChange} />
<input name="email" value={formData.email} onChange={handleChange} />
<input name="age" type="number" value={formData.age} onChange={handleChange} />
</form>
);
}
🎨 Event Handler Patterns
As your applications grow, you'll need patterns for handling events in more sophisticated ways. Let's explore common patterns that make your code cleaner and more maintainable.
Pattern 1: Passing Parameters to Handlers
When rendering lists, you often need to pass item data to event handlers:
interface Item {
id: number;
name: string;
}
function ItemList() {
const [items, setItems] = useState<Item[]>([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
]);
// Handler that needs the item ID
const handleDelete = (id: number) => {
setItems(items.filter(item => item.id !== id));
};
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
{/* Arrow function to pass parameter */}
<button onClick={() => handleDelete(item.id)}>
Delete
</button>
</li>
))}
</ul>
);
}
Pattern 2: Accessing Event AND Custom Data
Sometimes you need both the event object and custom parameters:
function EventAndDataExample() {
const handleClick = (
event: React.MouseEvent<HTMLButtonElement>,
itemId: number,
itemName: string
) => {
event.preventDefault();
console.log(`Clicked ${itemName} (ID: ${itemId})`);
console.log(`Click position: ${event.clientX}, ${event.clientY}`);
};
return (
<button onClick={(e) => handleClick(e, 42, 'Special Item')}>
Click with data
</button>
);
}
Pattern 3: Curried Event Handlers
A curried function returns another function - great for cleaner JSX:
function CurriedHandlerExample() {
const [values, setValues] = useState<Record<string, string>>({});
// Curried handler - returns a function!
const handleFieldChange = (fieldName: string) =>
(event: React.ChangeEvent<HTMLInputElement>) => {
setValues(prev => ({
...prev,
[fieldName]: event.target.value
}));
};
return (
<form>
{/* Much cleaner JSX! */}
<input onChange={handleFieldChange('firstName')} />
<input onChange={handleFieldChange('lastName')} />
<input onChange={handleFieldChange('email')} />
</form>
);
}
💡 Pro Tip: Event Handler Factory
For complex scenarios, create a factory function that generates handlers:
// Handler factory
const createHandler = <T extends HTMLElement>(
action: (data: unknown) => void,
options?: { preventDefault?: boolean; stopPropagation?: boolean }
) => {
return (data: unknown) => (event: React.SyntheticEvent<T>) => {
if (options?.preventDefault) event.preventDefault();
if (options?.stopPropagation) event.stopPropagation();
action(data);
};
};
// Usage
const handleDelete = createHandler(
(id) => console.log('Deleting:', id),
{ preventDefault: true }
);
<button onClick={handleDelete(itemId)}>Delete</button>
✅ Form Validation
Form validation ensures users enter correct data before submission. React makes it easy to implement real-time validation with immediate feedback.
Basic Validation Pattern
interface FormErrors {
email?: string;
password?: string;
}
function ValidationExample() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState<FormErrors>({});
const [touched, setTouched] = useState<Record<string, boolean>>({});
// Validation functions
const validateEmail = (value: string): string | undefined => {
if (!value) return 'Email is required';
if (!/\S+@\S+\.\S+/.test(value)) return 'Email is invalid';
return undefined;
};
const validatePassword = (value: string): string | undefined => {
if (!value) return 'Password is required';
if (value.length < 8) return 'Password must be at least 8 characters';
if (!/[A-Z]/.test(value)) return 'Password must contain uppercase';
if (!/[0-9]/.test(value)) return 'Password must contain a number';
return undefined;
};
// Validate on change
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setEmail(value);
if (touched.email) {
setErrors(prev => ({ ...prev, email: validateEmail(value) }));
}
};
// Mark field as touched on blur
const handleEmailBlur = () => {
setTouched(prev => ({ ...prev, email: true }));
setErrors(prev => ({ ...prev, email: validateEmail(email) }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Validate all fields
const emailError = validateEmail(email);
const passwordError = validatePassword(password);
setErrors({ email: emailError, password: passwordError });
setTouched({ email: true, password: true });
if (!emailError && !passwordError) {
console.log('Form is valid!');
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="email"
value={email}
onChange={handleEmailChange}
onBlur={handleEmailBlur}
style={{
borderColor: touched.email && errors.email ? '#f44336' : '#ddd'
}}
/>
{touched.email && errors.email && (
<span style={{ color: '#f44336' }}>{errors.email}</span>
)}
</div>
<button type="submit">Submit</button>
</form>
);
}
Password Strength Indicator
A common UX pattern showing requirements as they're met:
function PasswordStrength() {
const [password, setPassword] = useState('');
const requirements = [
{ label: 'At least 8 characters', test: (p: string) => p.length >= 8 },
{ label: 'Contains uppercase', test: (p: string) => /[A-Z]/.test(p) },
{ label: 'Contains lowercase', test: (p: string) => /[a-z]/.test(p) },
{ label: 'Contains number', test: (p: string) => /[0-9]/.test(p) },
{ label: 'Contains special char', test: (p: string) => /[!@#$%^&*]/.test(p) },
];
const metCount = requirements.filter(r => r.test(password)).length;
const strength = metCount === 5 ? 'Strong' : metCount >= 3 ? 'Medium' : 'Weak';
return (
<div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{/* Strength bar */}
<div style={{ display: 'flex', gap: '4px', marginTop: '8px' }}>
{[1, 2, 3, 4, 5].map(i => (
<div
key={i}
style={{
height: '4px',
flex: 1,
background: i <= metCount
? metCount === 5 ? '#4CAF50' : metCount >= 3 ? '#FF9800' : '#f44336'
: '#ddd',
borderRadius: '2px'
}}
/>
))}
</div>
<p>Strength: {strength}</p>
{/* Requirements list */}
<ul>
{requirements.map((req, i) => (
<li key={i} style={{ color: req.test(password) ? '#4CAF50' : '#999' }}>
{req.test(password) ? '✓' : '○'} {req.label}
</li>
))}
</ul>
</div>
);
}
⚠️ Validation Best Practices
- Don't validate on every keystroke for complex rules - use blur or debounce
- Show errors after touch - don't show errors on pristine fields
- Validate on submit as a final check
- Clear errors when user starts fixing them
- Use ARIA attributes for accessibility
🚀 Advanced Event Handling
Let's explore sophisticated event handling patterns for complex interactions.
Keyboard Shortcuts
Implement keyboard navigation and shortcuts:
import { useEffect, useCallback } from 'react';
function KeyboardShortcuts() {
const handleKeyDown = useCallback((event: KeyboardEvent) => {
const isMac = navigator.platform.toUpperCase().includes('MAC');
const modKey = isMac ? event.metaKey : event.ctrlKey;
// Ctrl/Cmd + S = Save
if (modKey && event.key === 's') {
event.preventDefault();
console.log('Save triggered!');
}
// Ctrl/Cmd + K = Search
if (modKey && event.key === 'k') {
event.preventDefault();
console.log('Search opened!');
}
// Escape = Close
if (event.key === 'Escape') {
console.log('Escape - close modal');
}
}, []);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown]);
return <div>Press Ctrl/Cmd + S to save</div>;
}
🎮 Interactive Demo: Event Tracker
Move your mouse and click to see events in real-time:
Move mouse and click to see events
Debouncing Events
Prevent excessive function calls during rapid input:
import { useState, useEffect } from 'react';
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
function SearchWithDebounce() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
console.log('Searching for:', debouncedQuery);
// API call here
}
}, [debouncedQuery]);
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
🎓 Section Summary
- Use
useEffectfor global keyboard shortcuts - Always cleanup event listeners in useEffect return
- Debounce expensive operations like API calls
- Use
useCallbackfor stable handler references
🏋️ Hands-on Practice
Time to put everything together! Let's build some practical examples that combine all the event handling concepts we've learned.
🏋️ Exercise 1: Interactive Todo List
Goal: Create a todo list with add, complete, and delete functionality.
Requirements:
- Input field to add new todos
- Button to submit new todos
- List of todos with checkboxes to mark complete
- Delete button for each todo
- Show count of remaining todos
- Prevent adding empty todos
💡 Hint
Start with this structure:
interface Todo {
id: number;
text: string;
completed: boolean;
}
const TodoList: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [inputValue, setInputValue] = useState('');
const handleAddTodo = (e: React.FormEvent) => {
e.preventDefault();
if (inputValue.trim()) {
// Add new todo
}
};
// Implement toggle and delete handlers
};
✅ Solution
import React, { useState } from 'react';
interface Todo {
id: number;
text: string;
completed: boolean;
}
const TodoList: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [inputValue, setInputValue] = useState('');
const handleAddTodo = (e: React.FormEvent) => {
e.preventDefault();
const trimmedValue = inputValue.trim();
if (trimmedValue) {
const newTodo: Todo = {
id: Date.now(),
text: trimmedValue,
completed: false
};
setTodos([...todos, newTodo]);
setInputValue('');
}
};
const handleToggleTodo = (id: number) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
const handleDeleteTodo = (id: number) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const remainingTodos = todos.filter(todo => !todo.completed).length;
return (
<div style={{ maxWidth: '500px', margin: '0 auto' }}>
<h2>My Todo List</h2>
<form onSubmit={handleAddTodo} style={{ marginBottom: '1rem' }}>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add a new todo..."
style={{
padding: '0.5rem',
width: '70%',
marginRight: '0.5rem'
}}
/>
<button type="submit" style={{ padding: '0.5rem 1rem' }}>
Add
</button>
</form>
<p>{remainingTodos} task{remainingTodos !== 1 ? 's' : ''} remaining</p>
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos.map(todo => (
<li
key={todo.id}
style={{
padding: '0.75rem',
marginBottom: '0.5rem',
background: '#f5f5f5',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<label style={{ display: 'flex', alignItems: 'center', flex: 1 }}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggleTodo(todo.id)}
style={{ marginRight: '0.5rem' }}
/>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
color: todo.completed ? '#999' : '#333'
}}
>
{todo.text}
</span>
</label>
<button
onClick={() => handleDeleteTodo(todo.id)}
style={{
background: '#f44336',
color: 'white',
border: 'none',
padding: '0.25rem 0.5rem',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Delete
</button>
</li>
))}
</ul>
{todos.length === 0 && (
<p style={{ textAlign: 'center', color: '#999' }}>
No todos yet. Add one to get started!
</p>
)}
</div>
);
};
export default TodoList;
🏋️ Exercise 2: Character Counter with Validation
Goal: Create a text area with character counting and validation feedback.
Requirements:
- Text area for user input
- Display current character count
- Maximum character limit (e.g., 280 characters)
- Visual feedback when approaching/exceeding limit
- Prevent submission if over limit
- Clear button to reset
💡 Hint
Use state for the text value and calculate the count:
const [text, setText] = useState('');
const maxLength = 280;
const remaining = maxLength - text.length;
const isOverLimit = remaining < 0;
const isWarning = remaining < 20 && remaining >= 0;
✅ Solution
import React, { useState } from 'react';
const CharacterCounter: React.FC = () => {
const [text, setText] = useState('');
const maxLength = 280;
const remaining = maxLength - text.length;
const isOverLimit = remaining < 0;
const isWarning = remaining < 20 && remaining >= 0;
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setText(e.target.value);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!isOverLimit && text.trim()) {
console.log('Submitted:', text);
alert('Message submitted!');
setText('');
}
};
const handleClear = () => {
setText('');
};
const getCounterColor = () => {
if (isOverLimit) return '#f44336';
if (isWarning) return '#ff9800';
return '#4CAF50';
};
return (
<div style={{ maxWidth: '600px', margin: '0 auto' }}>
<h2>Post a Message</h2>
<form onSubmit={handleSubmit}>
<textarea
value={text}
onChange={handleChange}
placeholder="What's on your mind?"
style={{
width: '100%',
minHeight: '120px',
padding: '0.75rem',
fontSize: '1rem',
border: `2px solid ${isOverLimit ? '#f44336' : '#ddd'}`,
borderRadius: '4px',
resize: 'vertical'
}}
/>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: '0.5rem'
}}>
<span
style={{
color: getCounterColor(),
fontWeight: 'bold',
fontSize: '1.1rem'
}}
>
{remaining} characters {isOverLimit ? 'over limit' : 'remaining'}
</span>
<div>
<button
type="button"
onClick={handleClear}
style={{
marginRight: '0.5rem',
padding: '0.5rem 1rem',
background: '#999',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Clear
</button>
<button
type="submit"
disabled={isOverLimit || !text.trim()}
style={{
padding: '0.5rem 1rem',
background: isOverLimit || !text.trim() ? '#ccc' : '#667eea',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: isOverLimit || !text.trim() ? 'not-allowed' : 'pointer'
}}
>
Post
</button>
</div>
</div>
{isOverLimit && (
<p style={{ color: '#f44336', marginTop: '0.5rem' }}>
⚠️ Your message is too long. Please shorten it.
</p>
)}
</form>
</div>
);
};
export default CharacterCounter;
🏋️ Exercise 3: Multi-Step Form
Goal: Create a multi-step form with navigation and validation.
Requirements:
- Three steps: Personal Info, Account Details, Preferences
- Next/Previous buttons to navigate
- Validate each step before proceeding
- Progress indicator showing current step
- Final review and submit
💡 Hint
Use state to track the current step and form data:
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState({
name: '',
email: '',
username: '',
password: '',
theme: 'light',
notifications: true
});
✅ Solution
import React, { useState } from 'react';
interface FormData {
name: string;
email: string;
username: string;
password: string;
theme: 'light' | 'dark';
notifications: boolean;
}
const MultiStepForm: React.FC = () => {
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
username: '',
password: '',
theme: 'light',
notifications: true
});
const [errors, setErrors] = useState<Partial<FormData>>({});
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value, type } = e.target;
const checked = (e.target as HTMLInputElement).checked;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
// Clear error for this field
setErrors(prev => ({ ...prev, [name]: undefined }));
};
const validateStep = (step: number): boolean => {
const newErrors: Partial<FormData> = {};
if (step === 1) {
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
} else if (step === 2) {
if (!formData.username.trim()) {
newErrors.username = 'Username is required';
} else if (formData.username.length < 3) {
newErrors.username = 'Username must be at least 3 characters';
}
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 6) {
newErrors.password = 'Password must be at least 6 characters';
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleNext = () => {
if (validateStep(currentStep)) {
setCurrentStep(prev => prev + 1);
}
};
const handlePrevious = () => {
setCurrentStep(prev => prev - 1);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Form submitted:', formData);
alert('Form submitted successfully!');
};
const renderStep = () => {
switch (currentStep) {
case 1:
return (
<div>
<h3>Personal Information</h3>
<div style={{ marginBottom: '1rem' }}>
<label>
Name:
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
style={{
display: 'block',
width: '100%',
padding: '0.5rem',
marginTop: '0.25rem'
}}
/>
</label>
{errors.name && (
<span style={{ color: '#f44336', fontSize: '0.875rem' }}>
{errors.name}
</span>
)}
</div>
<div>
<label>
Email:
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
style={{
display: 'block',
width: '100%',
padding: '0.5rem',
marginTop: '0.25rem'
}}
/>
</label>
{errors.email && (
<span style={{ color: '#f44336', fontSize: '0.875rem' }}>
{errors.email}
</span>
)}
</div>
</div>
);
case 2:
return (
<div>
<h3>Account Details</h3>
<div style={{ marginBottom: '1rem' }}>
<label>
Username:
<input
type="text"
name="username"
value={formData.username}
onChange={handleInputChange}
style={{
display: 'block',
width: '100%',
padding: '0.5rem',
marginTop: '0.25rem'
}}
/>
</label>
{errors.username && (
<span style={{ color: '#f44336', fontSize: '0.875rem' }}>
{errors.username}
</span>
)}
</div>
<div>
<label>
Password:
<input
type="password"
name="password"
value={formData.password}
onChange={handleInputChange}
style={{
display: 'block',
width: '100%',
padding: '0.5rem',
marginTop: '0.25rem'
}}
/>
</label>
{errors.password && (
<span style={{ color: '#f44336', fontSize: '0.875rem' }}>
{errors.password}
</span>
)}
</div>
</div>
);
case 3:
return (
<div>
<h3>Preferences</h3>
<div style={{ marginBottom: '1rem' }}>
<label>
Theme:
<select
name="theme"
value={formData.theme}
onChange={handleInputChange}
style={{
display: 'block',
width: '100%',
padding: '0.5rem',
marginTop: '0.25rem'
}}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</label>
</div>
<div>
<label style={{ display: 'flex', alignItems: 'center' }}>
<input
type="checkbox"
name="notifications"
checked={formData.notifications}
onChange={handleInputChange}
style={{ marginRight: '0.5rem' }}
/>
Enable email notifications
</label>
</div>
</div>
);
case 4:
return (
<div>
<h3>Review Your Information</h3>
<div style={{ background: '#f5f5f5', padding: '1rem', borderRadius: '4px' }}>
<p><strong>Name:</strong> {formData.name}</p>
<p><strong>Email:</strong> {formData.email}</p>
<p><strong>Username:</strong> {formData.username}</p>
<p><strong>Theme:</strong> {formData.theme}</p>
<p><strong>Notifications:</strong> {formData.notifications ? 'Enabled' : 'Disabled'}</p>
</div>
</div>
);
default:
return null;
}
};
return (
<div style={{ maxWidth: '500px', margin: '0 auto' }}>
<h2>Registration Form</h2>
{/* Progress indicator */}
<div style={{ marginBottom: '2rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
{[1, 2, 3, 4].map(step => (
<div
key={step}
style={{
width: '22%',
height: '8px',
background: step <= currentStep ? '#667eea' : '#ddd',
borderRadius: '4px'
}}
/>
))}
</div>
<p style={{ textAlign: 'center', color: '#666' }}>
Step {currentStep} of 4
</p>
</div>
<form onSubmit={handleSubmit}>
{renderStep()}
<div style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: '2rem'
}}>
{currentStep > 1 && (
<button
type="button"
onClick={handlePrevious}
style={{
padding: '0.5rem 1rem',
background: '#999',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Previous
</button>
)}
{currentStep < 4 ? (
<button
type="button"
onClick={handleNext}
style={{
padding: '0.5rem 1rem',
background: '#667eea',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginLeft: 'auto'
}}
>
Next
</button>
) : (
<button
type="submit"
style={{
padding: '0.5rem 1rem',
background: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
marginLeft: 'auto'
}}
>
Submit
</button>
)}
</div>
</form>
</div>
);
};
export default MultiStepForm;
✨ Best Practices
Let's wrap up with some professional patterns and best practices for event handling in React.
✅ Do: Use TypeScript Event Types
// Good - Explicit typing
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget.value);
};
// Better - Let TypeScript infer when possible
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
setValue(e.target.value);
};
⚠️ Don't: Call Functions in JSX
// Bad - Function is called immediately on render
<button onClick={handleClick()}>Click</button>
// Good - Pass function reference
<button onClick={handleClick}>Click</button>
// Good - Use arrow function for parameters
<button onClick={() => handleClick(id)}>Click</button>
✅ Do: Prevent Default When Needed
// Good - Always prevent default for forms
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Handle submission
};
// Good - Prevent default for links when needed
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
// Custom navigation logic
};
⚠️ Watch Out: Event Pooling (Legacy)
In older React versions, synthetic events were pooled. This is no longer an issue in React 17+, but if you're working with legacy code:
// React 16 and earlier - needed to persist
const handleClick = (e: React.MouseEvent) => {
e.persist(); // Not needed in React 17+
setTimeout(() => {
console.log(e.target); // Would be null without persist()
}, 1000);
};
// React 17+ - no persist needed
const handleClick = (e: React.MouseEvent) => {
setTimeout(() => {
console.log(e.target); // Works fine!
}, 1000);
};
✅ Do: Use Controlled Components for Forms
// Good - Controlled component
const [value, setValue] = useState('');
<input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
// Avoid - Uncontrolled (use refs sparingly)
<input defaultValue="initial" />
✅ Do: Validate Early and Often
// Good - Validate on change for immediate feedback
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const email = e.target.value;
setEmail(email);
// Immediate validation feedback
if (email && !isValidEmail(email)) {
setEmailError('Please enter a valid email');
} else {
setEmailError('');
}
};
// Also validate on blur
const handleEmailBlur = () => {
if (email && !isValidEmail(email)) {
setEmailError('Please enter a valid email');
}
};
// Final validation on submit
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!isValidEmail(email)) {
setEmailError('Please enter a valid email');
return;
}
// Proceed with submission
};
✅ Do: Debounce Expensive Operations
import { useState, useEffect } from 'react';
const SearchComponent: React.FC = () => {
const [searchTerm, setSearchTerm] = useState('');
const [debouncedTerm, setDebouncedTerm] = useState('');
// Debounce the search term
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedTerm(searchTerm);
}, 500);
return () => clearTimeout(timer);
}, [searchTerm]);
// Perform search when debounced term changes
useEffect(() => {
if (debouncedTerm) {
// Expensive API call or search operation
performSearch(debouncedTerm);
}
}, [debouncedTerm]);
return (
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
};
💡 Performance Tip: Memoize Event Handlers
import { useCallback } from 'react';
// Good - Memoized handler won't cause child re-renders
const handleClick = useCallback((id: number) => {
console.log('Clicked item:', id);
}, []); // Dependencies array
<ChildComponent onClick={handleClick} />
🎯 Event Handling Checklist
| Check | Description |
|---|---|
| ✅ Type all handlers | Use proper TypeScript event types |
| ✅ Prevent defaults | Call e.preventDefault() for forms and links |
| ✅ Validate inputs | Provide immediate feedback on user input |
| ✅ Handle errors gracefully | Show clear error messages to users |
| ✅ Use controlled components | Keep form state in React, not the DOM |
| ✅ Debounce expensive operations | Don't trigger API calls on every keystroke |
| ✅ Clean up side effects | Remove event listeners and clear timers |
| ✅ Test edge cases | Empty inputs, special characters, rapid clicking |
| ✅ Consider accessibility | Support keyboard navigation and screen readers |
| ✅ Optimize performance | Memoize handlers that are passed to child components |
🎓 Key Takeaways
- Always type your event handlers with TypeScript for type safety
- Use controlled components to keep form state in React
- Prevent default behavior when handling forms and navigation
- Validate user input early and provide clear feedback
- Debounce expensive operations like API calls
- Remember that React events are synthetic but work like native events
- Consider accessibility - support keyboard navigation
- Test your event handlers with edge cases and error conditions
📚 Summary
Congratulations! You've mastered event handling in React with TypeScript. Let's review what you've learned:
What You Learned
✅ Event Fundamentals
- Understanding React's synthetic event system
- Common event types: click, change, submit, keyboard, focus
- How events work with the virtual DOM
- Event bubbling and capturing in React
✅ TypeScript Integration
- Typing event handlers with
React.MouseEvent,React.ChangeEvent, etc. - Using generic types for specific elements
- Type-safe event handler patterns
- Inferring types from function signatures
✅ Form Handling
- Building controlled components
- Managing form state with useState
- Handling text inputs, checkboxes, radio buttons, and selects
- Form validation patterns and error handling
- Preventing default form submission
✅ Advanced Patterns
- Passing parameters to event handlers
- Event delegation and propagation control
- Keyboard event handling and shortcuts
- Focus management and accessibility
- Drag and drop interactions
- Debouncing expensive operations
✅ Best Practices
- Using controlled components for predictable state
- Validating early and providing immediate feedback
- Handling errors gracefully
- Optimizing performance with memoization
- Supporting keyboard navigation and accessibility
- Testing edge cases and error conditions
🎯 Skills You Can Now Apply
- Build interactive forms with validation
- Create responsive, user-friendly interfaces
- Handle user input confidently with TypeScript
- Implement complex interaction patterns
- Debug event-related issues effectively
- Write accessible, keyboard-navigable components
🚀 Next Steps
Now that you've mastered React basics and event handling, you're ready to move on to more advanced topics:
📖 Coming Up Next
Module 3: State and Interactivity
- Complex state management patterns
- Working with arrays and objects in state
- Managing related state values
- State updates and immutability
- Lifting state up and prop drilling
These concepts will build directly on what you've learned about events and state!
💪 Keep Practicing
The best way to solidify these concepts is to practice. Try building:
- A contact form with multiple fields and validation
- A filterable product list with search and category filters
- A quiz application with multiple choice questions
- A simple calculator with button clicks and keyboard support
- A comment system with nested replies
✨ Remember
Event handling is at the heart of interactive React applications. The patterns you've learned here - controlled components, proper typing, validation, and accessibility - will serve you throughout your React journey. Keep these principles in mind as you build more complex applications!
📚 Additional Resources
🎉 Congratulations!
You've completed Lesson 2.5 and finished Module 2: React Basics! You now have a solid foundation in React fundamentals, from components and props to styling and events. You're ready to tackle more advanced React concepts. Great work! 🚀