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 the things that happen in your app: a user clicks a button, types in a field, submits a form, or moves their mouse. If a component is a stage, events are the cues the audience sends back to it. Without them, your UI is a poster - pretty, but frozen. With them, it becomes a conversation.

Here is why this lesson matters: almost every feature you will ever build - a login form, a search box, a like button, a shopping cart - is, at its core, "user does X, so the app does Y." That "so the app does Y" is an event handler. Master event handling and you have mastered the mechanism behind virtually all interactivity. The good news? React makes it remarkably consistent, and TypeScript makes it remarkably safe.

๐Ÿ“– 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, and onSubmit. React calls your function and hands it an event object describing what happened.

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 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: a function reference in React ({handleClick}), a string of code in HTML ("handleClick()")
  • Preventing default: call e.preventDefault() in React; returning false from a handler does nothing in React
  • Consistency: React normalizes events across browsers so they behave identically everywhere

Heads-up (2026): the old onKeyPress event was deprecated and removed from the DOM - always reach for onKeyDown or onKeyUp instead.

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 event you will write most often: the humble click. The pattern is always the same three parts - define a function, attach it to an element with an on... prop, and let React call it when the action happens. Once this pattern clicks (pun intended), every other event works exactly the same way.

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

Here is a subtle but important fact: when your onClick handler runs, the event object React hands you is not the raw browser event. React wraps it in a "synthetic event." Think of it like a universal power adapter - the electricity is the same, but the plug is standardized so it fits every socket the same way.

๐Ÿ“– Definition

Synthetic Event: React's cross-browser wrapper around the browser's native event. It exposes the same interface as a native event (preventDefault, target, clientX, and so on) but smooths over the quirks so it behaves identically in every browser. Need the raw browser event? It is always available on event.nativeEvent.

There is also a performance story here. React does not attach a separate listener to every button and input on your page. Instead, since React 17 it attaches a small number of listeners to the root container where your app is mounted, and lets events bubble up to it - a technique called event delegation. Your components feel like they each have their own listeners, but under the hood React is efficiently routing one stream of events to the right handlers.

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: a Historical Footnote

You may still find blog posts warning you to call event.persist() before using an event asynchronously. That advice is obsolete. In React 16 and earlier, synthetic events were pooled - reused and wiped clean right after the handler finished - to save memory. React 17 removed pooling entirely, and React 19 has no trace of it. So in any modern app you can hold onto an event and read it later without ceremony:

// React 17+ (including React 19) - this just works
function ModernComponent() {
    const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
        setTimeout(() => {
            console.log(event.type); // โœ… Still readable a second later
        }, 1000);
    };

    return <button onClick={handleClick}>Click me</button>;
}

The takeaway: if you see event.persist() in a tutorial, you are reading something written for a version of React that is now several years old.

๐Ÿ”ท TypeScript Event Types

This is where TypeScript really earns its keep. Every event handler receives an event object, and TypeScript has a precisely-shaped type for each kind. Type your handler correctly and your editor instantly knows that event.target.value is a string on an input, that event.key exists on a keyboard event, and that event.clientX lives on a mouse event. Type it wrong (or not at all) and you lose that autocomplete - and you risk reading a property that does not exist.

All of React's event types are generic, written as React.SomeEvent<TElement>. That type parameter is the HTML element the handler is attached to. Providing it (for example HTMLInputElement) is what lets TypeScript know that event.currentTarget is specifically an input with a .value, not just a generic element. Think of the generic slot as telling TypeScript "which kind of element am I listening on?"

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

Passing Typed Handlers as Props

Handlers rarely live only in the component that uses them. Often a parent owns the logic and passes a handler down to a child button or input. When you do that, type the prop so the child knows exactly what shape of function to expect. React ships handy shorthand types - React.MouseEventHandler<T>, React.ChangeEventHandler<T>, and friends - for exactly this:

// Child declares the prop type
interface FancyButtonProps {
    label: string;
    onClick: React.MouseEventHandler<HTMLButtonElement>;
}

function FancyButton({ label, onClick }: FancyButtonProps) {
    return <button onClick={onClick}>{label}</button>;
}

// Parent passes a handler down - fully type-checked at the boundary
function Toolbar() {
    const handleSave = (event: React.MouseEvent<HTMLButtonElement>) => {
        console.log('Saving from', event.currentTarget.textContent);
    };

    return <FancyButton label="Save" onClick={handleSave} />;
}

Because onClick is typed, the parent gets an error if it passes a handler with the wrong signature, and the child gets full autocomplete on the event inside its own JSX. The type is the contract between the two components.

โœ… TypeScript Benefits

  • Autocomplete - your IDE suggests available event properties as you type event.
  • Type checking - catch mismatched handlers and impossible property reads before runtime
  • Documentation - the type signature shows exactly what a handler expects and returns
  • Refactoring - rename or reshape an event flow and TypeScript flags every call site that breaks

๐ŸŽฏ 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: A Controlled Input in Action

The diagram above is the theory; here is the practice. Type in the field below and watch React state track every keystroke in real time. This is a controlled component: the displayed state is the single source of truth, and the input simply reflects it back.

const [value, setValue] = useState('') // <input value={value} onChange={e => setValue(e.target.value)} />

React state is currently empty โ€” start typing to update it.

โœ… 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) => {
        // navigator.platform is deprecated; sniff the user agent instead
        const isMac = /Mac|iPhone|iPad/i.test(navigator.userAgent);
        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: Live Event Playground

Interact with the target below โ€” click it, double-click it, hover on and off, or focus it and press a key. Each interaction fires its handler and logs what happened, exactly like the synthetic events your React handlers receive. Notice how one element can respond to many different event types at once.

onClick ยท onDoubleClick ยท onMouseEnter ยท onMouseLeave ยท onKeyDown

๐Ÿ‘† Interact with the target to see events appear here.

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, attaching the listener to document
  • Always clean up event listeners in the useEffect return function - otherwise they pile up on every render
  • Debounce expensive operations like API calls so rapid typing does not flood your backend
  • Use useCallback for stable handler references, especially when a handler is a useEffect dependency

๐Ÿ‹๏ธ 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