Skip to main content

⚡ 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, return false in HTML
  • Consistency: React normalizes events across browsers
graph LR A[User Action] --> B[Browser Event] B --> C[React Synthetic Event] C --> D[Your Event Handler] D --> E[Update State/UI] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style E fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff

🖱️ 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.

graph TD A[Child Element Clicked] --> B{stopPropagation called?} B -->|No| C[Event bubbles to Parent] B -->|Yes| D[Event stops here] C --> E[Parent handler runs] E --> F[Continues bubbling up] style A fill:#667eea,stroke:#333,stroke-width:2px,color:#fff style D fill:#4CAF50,stroke:#333,stroke-width:2px,color:#fff style F fill:#ffc107,stroke:#333,stroke-width:2px

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:

What type of interaction? 🖱️ Mouse Events React.MouseEvent<T> onClick, onDoubleClick onMouseEnter, onMouseLeave onMouseMove, onMouseDown ⌨️ Keyboard Events React.KeyboardEvent<T> onKeyDown, onKeyUp event.key, event.code event.ctrlKey, event.shiftKey 📝 Form Events React.ChangeEvent<T> React.FormEvent<T> onChange, onSubmit event.target.value 🎯 Focus Events React.FocusEvent<T> onFocus, onBlur Validation triggers Accessibility support Common Element Type Parameters (T) HTMLButtonElement <button> HTMLInputElement <input> HTMLFormElement <form> HTMLDivElement <div>, etc. 💡 Pro Tip: Hover over event types in your IDE to see all available properties!

📝 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

Data Flow Comparison ✅ Controlled Component (Recommended) React State useState() Input Element value={state} value onChange → setState() ✓ Single source of truth ✓ Instant validation ✓ Conditional disabling ✓ Format as you type ⚠️ Uncontrolled Component DOM Stores value useRef() Access on submit ref.current ✗ No instant validation ✗ Can't track changes ✗ Harder to test ✓ Good for file inputs

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.

Common Event Handler Patterns 1. Passing Parameters onClick={() => handleDelete(id)} • Use arrow function wrapper • Pass data to handler • Common for lists items.map(item => ...) 2. Event + Parameters (e) => handleClick(e, id) • Access event object • AND pass custom data • preventDefault + data Flexible & powerful 3. Curried Handler const handler = (id) => (e) => doSomething(id) • Returns function • Cleaner JSX • Reusable pattern onClick={handler(id)} 4. Conditional Handling disabled ? undefined : fn • Enable/disable handlers • Based on state • Loading states onClick={!loading && handler} 5. Composed Handlers onClick={(e) => { trackAnalytics(); handleClick(e); }} • Multiple actions • Logging, analytics Run multiple functions 6. Memoized Handler const handler = useCallback( () => doSomething(), [deps] ); • Prevent re-renders • Child optimization Performance pattern

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.

Validation Timing Options onChange Validate every keystroke ✓ Instant feedback onBlur Validate when leaving field ✓ Less intrusive onSubmit Validate on form submit ✓ All at once Combined ⭐ All three for best UX ✓ Recommended Validation States Pristine Not touched yet Dirty User has typed Invalid Has errors Valid Ready to submit

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 useEffect for global keyboard shortcuts
  • Always cleanup event listeners in useEffect return
  • Debounce expensive operations like API calls
  • Use useCallback for 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!

🎉 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! 🚀

← Previous Styling in React Home Next → Module 2 Project