Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
14 minLesson 6 of 33
React Core Concepts

Event Handling & Synthetic Events

Event Handling & Synthetic Events

React's event system wraps the browser's native events in a cross-browser abstraction called a SyntheticEvent. For most purposes it behaves identically to the native event — but knowing how it works prevents subtle bugs, especially around event pooling, bubbling, and asynchronous handlers.

Attaching Event Handlers

Pass a function to the event prop — not a string like in old HTML:

// ❌ HTML-style string (doesn't work in React)
<button onclick="handleClick()">Click</button>

// ✅ React — pass the function reference
<button onClick={handleClick}>Click</button>

// Or inline arrow function
<button onClick={() => console.log("clicked")}>Click</button>

// With an argument
<button onClick={() => handleDelete(item.id)}>Delete</button>

The function receives a SyntheticEvent as its first argument:

function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
    e.preventDefault();
    console.log(e.target, e.currentTarget);
}

Common Events

// Mouse events
<div onClick={handleClick}>
<div onDoubleClick={handleDoubleClick}>
<div onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)}>
<div onMouseDown={handleDragStart} onMouseUp={handleDragEnd}>

// Keyboard events
<input onKeyDown={handleKeyDown} onKeyUp={handleKeyUp} onKeyPress={handleKeyPress} />

// Focus events
<input onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} />

// Form events
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<select onChange={handleChange}>

// Drag events
<div draggable onDragStart={handleDragStart} onDrop={handleDrop} onDragOver={e => e.preventDefault()}>

// Touch events (mobile)
<div onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd}>

Handling Form Events

The most common event — reading input values:

function ContactForm() {
    const [form, setForm] = useState({ name: "", email: "", message: "" });
    
    // Generic handler for all text inputs
    function handleChange(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) {
        const { name, value } = e.target;
        setForm(prev => ({ ...prev, [name]: value }));
    }
    
    function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
        e.preventDefault();   // prevent page reload
        
        // form.name, form.email, form.message are ready
        submitForm(form);
    }
    
    return (
        <form onSubmit={handleSubmit}>
            <input name="name" value={form.name} onChange={handleChange} />
            <input name="email" type="email" value={form.email} onChange={handleChange} />
            <textarea name="message" value={form.message} onChange={handleChange} />
            <button type="submit">Send</button>
        </form>
    );
}

Keyboard Events

Respond to specific keys:

function SearchInput() {
    const [query, setQuery] = useState("");
    
    function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
        if (e.key === "Enter") {
            performSearch(query);
        }
        if (e.key === "Escape") {
            setQuery("");
        }
        // Modifier keys
        if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
            e.preventDefault();
            openCommandPalette();
        }
    }
    
    return (
        <input
            value={query}
            onChange={e => setQuery(e.target.value)}
            onKeyDown={handleKeyDown}
            placeholder="Search..."
        />
    );
}

Event Bubbling and Stopping Propagation

Events bubble up the DOM tree by default. A click on a button inside a card triggers the card's onClick too:

function CourseCard({ course, onCardClick, onEnrollClick }: Props) {
    return (
        <div onClick={onCardClick} className="cursor-pointer">
            <h3>{course.title}</h3>
            
            {/* This click would also trigger onCardClick! */}
            <button
                onClick={(e) => {
                    e.stopPropagation();   // stop bubbling to the parent div
                    onEnrollClick(course.id);
                }}
            >
                Enroll
            </button>
        </div>
    );
}

e.stopPropagation() stops the event from reaching parent handlers. e.preventDefault() stops the browser's default behavior (form submitting, link navigating, etc.).

They're independent — you can call one, both, or neither.

Event Delegation and Performance

React uses event delegation — it attaches a single listener to the root instead of one per element. This means even a list of 10,000 items has only one event listener:

// This is fine for large lists — React handles it efficiently
function ItemList({ items }: { items: Item[] }) {
    function handleClick(id: string) {
        setSelected(id);
    }
    
    return (
        <ul>
            {items.map(item => (
                // No performance problem having onClick in a large list
                <li key={item.id} onClick={() => handleClick(item.id)}>
                    {item.name}
                </li>
            ))}
        </ul>
    );
}

Custom Events Between Components

React doesn't have custom events like the DOM does — instead, pass callback props:

// "Events" in React are just callback functions

function SearchBar({ onSearch }: { onSearch: (query: string) => void }) {
    const [query, setQuery] = useState("");
    
    return (
        <form onSubmit={e => { e.preventDefault(); onSearch(query); }}>
            <input value={query} onChange={e => setQuery(e.target.value)} />
            <button type="submit">Search</button>
        </form>
    );
}

function CoursesPage() {
    const [results, setResults] = useState<Course[]>([]);
    
    async function handleSearch(query: string) {
        const data = await fetchCourses({ search: query });
        setResults(data);
    }
    
    return (
        <div>
            <SearchBar onSearch={handleSearch} />
            <CourseGrid courses={results} />
        </div>
    );
}

The onClick Trap with Async Functions

// ❌ This looks fine but has a problem
<button onClick={async () => {
    setLoading(true);
    await submitForm(data);
    setLoading(false);
}}>
    Submit
</button>

// If the user clicks multiple times quickly, you can get race conditions
// Better: disable while loading
<button
    disabled={loading}
    onClick={async () => {
        setLoading(true);
        try {
            await submitForm(data);
        } finally {
            setLoading(false);
        }
    }}
>
    {loading ? "Submitting..." : "Submit"}
</button>

Touch and Pointer Events

For drag-and-drop and touch interfaces:

function DraggableCard({ id, children }: { id: string; children: React.ReactNode }) {
    const [isDragging, setIsDragging] = useState(false);
    
    return (
        <div
            draggable
            onDragStart={(e) => {
                e.dataTransfer.setData("text/plain", id);
                setIsDragging(true);
            }}
            onDragEnd={() => setIsDragging(false)}
            className={`cursor-grab ${isDragging ? "opacity-50" : ""}`}
        >
            {children}
        </div>
    );
}

function DropZone({ onDrop }: { onDrop: (id: string) => void }) {
    const [isOver, setIsOver] = useState(false);
    
    return (
        <div
            onDragOver={e => { e.preventDefault(); setIsOver(true); }}
            onDragLeave={() => setIsOver(false)}
            onDrop={e => {
                e.preventDefault();
                setIsOver(false);
                const id = e.dataTransfer.getData("text/plain");
                onDrop(id);
            }}
            className={`border-2 border-dashed rounded-xl p-8 text-center transition-colors ${
                isOver ? "border-blue-500 bg-blue-50" : "border-gray-300"
            }`}
        >
            Drop here
        </div>
    );
}

Next lesson: Context API and useContext — sharing state across components without prop drilling.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!