Episode 2 — React Frontend Architecture NextJS / 2.5 — Event Handling and Conditional Rendering
2.5.a — Handling Click Events in React
In one sentence: React wraps browser events in a cross-browser SyntheticEvent system that lets you attach handlers declaratively in JSX, giving you a consistent API for clicks, keyboard input, mouse movement, and every other user interaction.
Navigation: ← Overview · Next → Controlled Inputs
Table of Contents
- How Events Work in the Browser (Quick Recap)
- React's Synthetic Event System
- Your First Click Handler
- The Event Object — What You Get
- Passing Arguments to Event Handlers
- Inline Handlers vs Named Handlers
- The "this" Problem (And Why It Doesn't Exist in Functional Components)
- Event Propagation — Bubbling and Capturing
- Stopping Propagation and Preventing Default
- Common Event Types in React
- Mouse Events Deep Dive
- Keyboard Events Deep Dive
- Focus and Blur Events
- Touch Events for Mobile
- Event Delegation in React — You Don't Need It
- TypeScript and Events
- Performance Considerations
- Common Mistakes and Anti-Patterns
- Real-World Examples
- Key Takeaways
1. How Events Work in the Browser (Quick Recap)
Before understanding React events, you need to know how the browser handles them natively.
The DOM Event Flow
When a user clicks a button, the browser doesn't just fire an event on that button. It follows a three-phase process:
Window
│
┌──────────┴──────────┐
│ CAPTURING │
│ (top → target) │
▼ │
Document │
│ │
<html> │
│ │
<body> │
│ │
<div> │
│ │
┌────────▼─────────┐ │
│ <button> │ ← TARGET │
│ (click here) │ │
└────────┬─────────┘ │
│ │
│ BUBBLING │
│ (target → top) │
└─────────────────────┘
Three phases:
| Phase | Direction | Description |
|---|---|---|
| Capture | Window → Target | Event travels DOWN the tree |
| Target | At the element | Event fires on the clicked element |
| Bubble | Target → Window | Event travels UP the tree |
Native JavaScript Event Handling
// Native DOM — imperative, manual
const button = document.querySelector('#myButton');
button.addEventListener('click', function(event) {
console.log('Clicked!', event.target);
});
// You also need to clean up:
button.removeEventListener('click', handler);
Problems with native events:
- Manual cleanup required (memory leaks)
- Cross-browser inconsistencies (IE vs Chrome vs Firefox)
- Event delegation is manual and error-prone
- No integration with component lifecycle
React solves ALL of these problems.
2. React's Synthetic Event System
React doesn't attach event listeners to individual DOM nodes. Instead, it uses a single event listener at the root of your app and delegates all events through its own system.
What is a SyntheticEvent?
A SyntheticEvent is React's cross-browser wrapper around the native browser event. It has the same interface as native events (stopPropagation(), preventDefault(), target, etc.) but works identically across all browsers.
┌─────────────────────────────────────────┐
│ React Root (#root) │
│ │
│ Single event listener for ALL events │
│ │
│ ┌─────────────────────────────────┐ │
│ │ User clicks <button> │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Native event captured │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ SyntheticEvent created │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ React finds component │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Calls your handler(event) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ SyntheticEvent recycled │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
SyntheticEvent vs Native Event
| Feature | Native Event | SyntheticEvent |
|---|---|---|
| Cross-browser | Inconsistent | Consistent API |
| Cleanup | Manual removeEventListener | Automatic on unmount |
| Performance | One listener per element | Single root listener |
| Access native event | Direct | event.nativeEvent |
| Event pooling | N/A | Removed in React 17+ |
| Works with JSX | No | Yes |
Accessing the Native Event
function HandleNative() {
function handleClick(event) {
// SyntheticEvent
console.log(event); // SyntheticEvent {...}
console.log(event.type); // "click"
// Access the real browser event
console.log(event.nativeEvent); // MouseEvent {...}
console.log(event.nativeEvent instanceof MouseEvent); // true
}
return <button onClick={handleClick}>Click me</button>;
}
3. Your First Click Handler
The Simplest Possible Handler
function ClickCounter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
Clicked {count} times
</button>
);
}
Key syntax rules:
// ✅ CORRECT: Pass a reference to the function
<button onClick={handleClick}>
// ❌ WRONG: Calling the function immediately
<button onClick={handleClick()}>
// This calls handleClick during RENDER, not on click!
// ✅ CORRECT: Arrow function wrapper (for passing arguments)
<button onClick={() => handleClick('hello')}>
// ❌ WRONG: String handler (this isn't HTML)
<button onClick="handleClick()">
React Event Names vs HTML
React uses camelCase for event names, not lowercase:
| HTML Attribute | React Prop |
|---|---|
onclick | onClick |
onchange | onChange |
onmouseover | onMouseOver |
onkeydown | onKeyDown |
onfocus | onFocus |
onsubmit | onSubmit |
ondragstart | onDragStart |
onscroll | onScroll |
4. The Event Object — What You Get
Every event handler receives a SyntheticEvent object as its first argument.
Common Properties
function EventInspector() {
function handleClick(event) {
// ─── Target Info ───
console.log(event.target); // The DOM element that was clicked
console.log(event.currentTarget); // The DOM element the handler is attached to
// ─── Event Info ───
console.log(event.type); // "click"
console.log(event.timeStamp); // When the event occurred
console.log(event.eventPhase); // 3 (bubbling)
// ─── Mouse Position ───
console.log(event.clientX); // X relative to viewport
console.log(event.clientY); // Y relative to viewport
console.log(event.pageX); // X relative to document
console.log(event.pageY); // Y relative to document
console.log(event.screenX); // X relative to screen
console.log(event.screenY); // Y relative to screen
// ─── Modifier Keys ───
console.log(event.altKey); // Was Alt held?
console.log(event.ctrlKey); // Was Ctrl held?
console.log(event.shiftKey); // Was Shift held?
console.log(event.metaKey); // Was Cmd/Win held?
// ─── Mouse Button ───
console.log(event.button); // 0=left, 1=middle, 2=right
// ─── Methods ───
event.preventDefault(); // Cancel default browser action
event.stopPropagation(); // Stop event from bubbling
}
return <button onClick={handleClick}>Inspect Event</button>;
}
target vs currentTarget
This is one of the most confusing aspects for beginners:
function TargetDemo() {
function handleClick(event) {
console.log('target:', event.target.tagName);
console.log('currentTarget:', event.currentTarget.tagName);
}
return (
<div onClick={handleClick} style={{ padding: 20, background: '#eee' }}>
{/* Click the span: target = SPAN, currentTarget = DIV */}
{/* Click the div padding: target = DIV, currentTarget = DIV */}
<span>Click me or click the padding</span>
</div>
);
}
┌──────────────── <div onClick={handleClick}> ────────────────┐
│ │
│ currentTarget is ALWAYS the div (where handler is) │
│ │
│ ┌──────────────── <span> ──────────────────┐ │
│ │ │ │
│ │ If you click HERE, target = span │ │
│ │ │ │
│ └───────────────────────────────────────────┘ │
│ │
│ If you click HERE (padding), target = div │
│ │
└──────────────────────────────────────────────────────────────┘
event.target | event.currentTarget | |
|---|---|---|
| Definition | Element that was actually clicked | Element the handler is on |
| Changes? | Yes — depends on where user clicked | No — always the handler element |
| Use for | Figuring out WHAT was clicked | Accessing the element with the handler |
5. Passing Arguments to Event Handlers
Method 1: Arrow Function Wrapper
function ItemList() {
const items = ['Apple', 'Banana', 'Cherry'];
function handleDelete(itemName) {
console.log(`Deleting ${itemName}`);
}
return (
<ul>
{items.map(item => (
<li key={item}>
{item}
{/* Arrow wrapper — creates new function each render */}
<button onClick={() => handleDelete(item)}>Delete</button>
</li>
))}
</ul>
);
}
Method 2: Currying (Function That Returns a Function)
function ItemList() {
const items = ['Apple', 'Banana', 'Cherry'];
// Curried handler
function handleDelete(itemName) {
return function (event) {
console.log(`Deleting ${itemName}`, event);
};
}
// Shorter with arrow functions
const handleDelete2 = (itemName) => (event) => {
console.log(`Deleting ${itemName}`, event);
};
return (
<ul>
{items.map(item => (
<li key={item}>
{item}
<button onClick={handleDelete(item)}>Delete</button>
</li>
))}
</ul>
);
}
Method 3: Data Attributes (No Extra Functions)
function ItemList() {
const items = ['Apple', 'Banana', 'Cherry'];
function handleDelete(event) {
const itemName = event.currentTarget.dataset.item;
console.log(`Deleting ${itemName}`);
}
return (
<ul>
{items.map(item => (
<li key={item}>
{item}
<button data-item={item} onClick={handleDelete}>Delete</button>
</li>
))}
</ul>
);
}
When to Use Which
| Method | Creates Functions | Extra Re-renders? | Best For |
|---|---|---|---|
| Arrow wrapper | Yes, every render | With React.memo yes | Small lists, simple handlers |
| Currying | Yes, every render | Same as arrow | When you need event + args |
| Data attributes | No | No | Large lists, performance-critical |
6. Inline Handlers vs Named Handlers
Inline Handler
function InlineExample() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
Named Handler
function NamedExample() {
const [count, setCount] = useState(0);
function handleIncrement() {
setCount(prev => prev + 1);
}
return (
<button onClick={handleIncrement}>
Count: {count}
</button>
);
}
Comparison
| Aspect | Inline | Named |
|---|---|---|
| Readability | Good for 1-liners | Better for complex logic |
| Debugging | Anonymous in stack traces | Named in stack traces |
| Reusability | Can't reuse | Can use in multiple JSX elements |
| Testing | Harder to test | Can export and test separately |
| Conventions | OK for simple setters | Preferred for anything complex |
Naming Convention
React has a strong convention for handler names:
// Pattern: handle + EventType or handle + What + EventType
function SearchForm() {
function handleSubmit(event) { /* ... */ } // Form event
function handleSearchChange(event) { /* ... */ } // Specific input
function handleReset() { /* ... */ } // Action
function handleFilterToggle() { /* ... */ } // Toggle action
// For props: on + EventType
return (
<ChildComponent
onSubmit={handleSubmit}
onSearchChange={handleSearchChange}
onReset={handleReset}
/>
);
}
Rule of thumb:
- Inside component:
handle+ Noun + Verb →handleFormSubmit - Props to children:
on+ Noun + Verb →onFormSubmit
7. The "this" Problem (And Why It Doesn't Exist in Functional Components)
In class components, this was a constant headache:
// ❌ CLASS COMPONENT — "this" problem
class BrokenButton extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
// Must bind "this" or it's undefined in the handler!
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// Without binding, "this" is undefined here
this.setState({ count: this.state.count + 1 });
}
render() {
return <button onClick={this.handleClick}>Count: {this.state.count}</button>;
}
}
// ✅ FUNCTIONAL COMPONENT — no "this" at all
function WorkingButton() {
const [count, setCount] = useState(0);
function handleClick() {
// No "this" — just use the variable directly
setCount(count + 1);
}
return <button onClick={handleClick}>Count: {count}</button>;
}
Why functional components don't have this problem:
- No class instance → no
this - State comes from
useState→ captured by closure - Handlers are regular functions →
thisdoesn't matter - Arrow functions in function bodies → always have correct scope
This is one of the major reasons React moved toward functional components.
8. Event Propagation — Bubbling and Capturing
Bubbling (Default Behavior)
Events bubble UP from the target to the root:
function BubblingDemo() {
return (
<div onClick={() => console.log('3. DIV clicked')}>
<ul onClick={() => console.log('2. UL clicked')}>
<li onClick={() => console.log('1. LI clicked')}>
Click me
</li>
</ul>
</div>
);
}
// Clicking the <li> logs:
// 1. LI clicked
// 2. UL clicked
// 3. DIV clicked
Capturing
To listen during the CAPTURE phase (top → down), append Capture to the event name:
function CapturingDemo() {
return (
<div onClickCapture={() => console.log('1. DIV capture')}>
<ul onClickCapture={() => console.log('2. UL capture')}>
<li onClick={() => console.log('3. LI bubble')}>
Click me
</li>
</ul>
</div>
);
}
// Clicking the <li> logs:
// 1. DIV capture (top-down)
// 2. UL capture (top-down)
// 3. LI bubble (bottom-up)
Full Order with Both Phases
function FullEventFlow() {
return (
<div
onClickCapture={() => console.log('1. outer CAPTURE')}
onClick={() => console.log('6. outer BUBBLE')}
>
<div
onClickCapture={() => console.log('2. middle CAPTURE')}
onClick={() => console.log('5. middle BUBBLE')}
>
<button
onClickCapture={() => console.log('3. target CAPTURE')}
onClick={() => console.log('4. target BUBBLE')}
>
Click
</button>
</div>
</div>
);
}
// Output order: 1, 2, 3, 4, 5, 6
9. Stopping Propagation and Preventing Default
stopPropagation — Stop Event from Reaching Parents
function StopPropagationDemo() {
function handleParentClick() {
console.log('Parent clicked — this should NOT fire');
}
function handleButtonClick(event) {
event.stopPropagation(); // Prevents bubbling to parent
console.log('Button clicked — only this fires');
}
return (
<div onClick={handleParentClick} style={{ padding: 40, background: '#eee' }}>
<button onClick={handleButtonClick}>Click Me</button>
</div>
);
}
preventDefault — Cancel Browser Default Action
function PreventDefaultDemo() {
// ─── Prevent form from reloading the page ───
function handleSubmit(event) {
event.preventDefault(); // No page reload
console.log('Form submitted via JavaScript');
}
// ─── Prevent link from navigating ───
function handleLinkClick(event) {
event.preventDefault(); // No navigation
console.log('Link clicked but not navigating');
}
// ─── Prevent context menu ───
function handleContextMenu(event) {
event.preventDefault(); // No right-click menu
console.log('Custom context menu instead');
}
return (
<div>
<form onSubmit={handleSubmit}>
<input type="text" />
<button type="submit">Submit</button>
</form>
<a href="https://google.com" onClick={handleLinkClick}>
Google (won't navigate)
</a>
<div onContextMenu={handleContextMenu}>
Right-click here for custom menu
</div>
</div>
);
}
stopPropagation vs preventDefault
| Method | What it does | Use case |
|---|---|---|
stopPropagation() | Stops event from reaching parent elements | Button inside clickable card |
preventDefault() | Cancels browser's default action | Form submit, link click, drag |
| Both together | Stops both bubbling AND default | Complex nested interactive elements |
Real-World: Button Inside a Clickable Card
function CardWithButton({ title, onCardClick, onDelete }) {
function handleDeleteClick(event) {
event.stopPropagation(); // Don't trigger card click
onDelete();
}
return (
<div onClick={onCardClick} className="card" style={{ cursor: 'pointer' }}>
<h3>{title}</h3>
<button onClick={handleDeleteClick}>🗑️ Delete</button>
</div>
);
}
10. Common Event Types in React
Complete Event Category Reference
┌─────────────────────────────────────────────────────────┐
│ React Event Categories │
├──────────────┬──────────────────────────────────────────┤
│ Category │ Events │
├──────────────┼──────────────────────────────────────────┤
│ Mouse │ onClick, onDoubleClick, onMouseDown, │
│ │ onMouseUp, onMouseMove, onMouseEnter, │
│ │ onMouseLeave, onMouseOver, onMouseOut │
├──────────────┼──────────────────────────────────────────┤
│ Keyboard │ onKeyDown, onKeyUp, onKeyPress (deprecated)│
├──────────────┼──────────────────────────────────────────┤
│ Form │ onChange, onInput, onSubmit, onReset, │
│ │ onInvalid │
├──────────────┼──────────────────────────────────────────┤
│ Focus │ onFocus, onBlur, onFocusCapture, │
│ │ onBlurCapture │
├──────────────┼──────────────────────────────────────────┤
│ Touch │ onTouchStart, onTouchMove, onTouchEnd, │
│ │ onTouchCancel │
├──────────────┼──────────────────────────────────────────┤
│ Clipboard │ onCopy, onCut, onPaste │
├──────────────┼──────────────────────────────────────────┤
│ Drag │ onDrag, onDragStart, onDragEnd, │
│ │ onDragEnter, onDragLeave, onDragOver, │
│ │ onDrop │
├──────────────┼──────────────────────────────────────────┤
│ Scroll │ onScroll │
├──────────────┼──────────────────────────────────────────┤
│ Wheel │ onWheel │
├──────────────┼──────────────────────────────────────────┤
│ Animation │ onAnimationStart, onAnimationEnd, │
│ │ onAnimationIteration │
├──────────────┼──────────────────────────────────────────┤
│ Transition │ onTransitionEnd │
├──────────────┼──────────────────────────────────────────┤
│ Media │ onPlay, onPause, onEnded, onLoadedData, │
│ │ onTimeUpdate, onVolumeChange │
├──────────────┼──────────────────────────────────────────┤
│ Image │ onLoad, onError │
├──────────────┼──────────────────────────────────────────┤
│ Composition │ onCompositionStart, onCompositionUpdate, │
│ │ onCompositionEnd │
├──────────────┼──────────────────────────────────────────┤
│ Pointer │ onPointerDown, onPointerMove, │
│ │ onPointerUp, onPointerCancel, │
│ │ onPointerEnter, onPointerLeave │
└──────────────┴──────────────────────────────────────────┘
11. Mouse Events Deep Dive
onClick — The Most Common Event
function ClickVariations() {
function handleClick(event) {
// Detect modifier keys for power-user features
if (event.metaKey || event.ctrlKey) {
console.log('Cmd/Ctrl + Click → open in new tab');
} else if (event.shiftKey) {
console.log('Shift + Click → select range');
} else {
console.log('Normal click');
}
}
return <button onClick={handleClick}>Smart Click</button>;
}
onDoubleClick
function DoubleClickEdit() {
const [isEditing, setIsEditing] = useState(false);
const [text, setText] = useState('Double-click me to edit');
if (isEditing) {
return (
<input
value={text}
onChange={e => setText(e.target.value)}
onBlur={() => setIsEditing(false)}
onKeyDown={e => e.key === 'Enter' && setIsEditing(false)}
autoFocus
/>
);
}
return <p onDoubleClick={() => setIsEditing(true)}>{text}</p>;
}
onMouseEnter vs onMouseOver
function HoverDemo() {
const [isHovered, setIsHovered] = useState(false);
return (
<div
// onMouseEnter/Leave — doesn't trigger for children
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
style={{
padding: 20,
background: isHovered ? '#e0f0ff' : '#f0f0f0',
transition: 'background 0.2s',
}}
>
<p>Hover over me</p>
<button>This button won't trigger extra events</button>
</div>
);
}
| Event | Bubbles? | When it fires |
|---|---|---|
onMouseEnter | No | Mouse enters element (ignores children) |
onMouseLeave | No | Mouse leaves element (ignores children) |
onMouseOver | Yes | Mouse enters element OR any child |
onMouseOut | Yes | Mouse leaves element OR any child |
Rule: Almost always use onMouseEnter/onMouseLeave — they're simpler and don't cause flicker.
Drag-and-Drop (Basic)
function DragDropDemo() {
const [items, setItems] = useState(['Item A', 'Item B', 'Item C']);
const [dragIndex, setDragIndex] = useState(null);
function handleDragStart(index) {
setDragIndex(index);
}
function handleDragOver(event) {
event.preventDefault(); // Required to allow drop
}
function handleDrop(dropIndex) {
const newItems = [...items];
const [dragged] = newItems.splice(dragIndex, 1);
newItems.splice(dropIndex, 0, dragged);
setItems(newItems);
setDragIndex(null);
}
return (
<ul>
{items.map((item, index) => (
<li
key={item}
draggable
onDragStart={() => handleDragStart(index)}
onDragOver={handleDragOver}
onDrop={() => handleDrop(index)}
style={{
padding: 10,
margin: 4,
background: dragIndex === index ? '#ccc' : '#fff',
border: '1px solid #ddd',
cursor: 'grab',
}}
>
{item}
</li>
))}
</ul>
);
}
12. Keyboard Events Deep Dive
onKeyDown vs onKeyUp
function KeyboardDemo() {
function handleKeyDown(event) {
console.log('Key down:', event.key, event.code);
// Common keyboard shortcuts
if (event.key === 'Enter') {
console.log('Enter pressed');
}
if (event.key === 'Escape') {
console.log('Escape pressed');
}
if ((event.metaKey || event.ctrlKey) && event.key === 's') {
event.preventDefault(); // Prevent browser save dialog
console.log('Cmd/Ctrl+S — save');
}
if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
event.preventDefault();
console.log('Cmd/Ctrl+K — open command palette');
}
}
return (
<div tabIndex={0} onKeyDown={handleKeyDown}>
<p>Focus here and press keys</p>
</div>
);
}
Key vs Code
// event.key — the CHARACTER produced (affected by keyboard layout)
// event.code — the PHYSICAL key (same regardless of layout)
// On US keyboard, pressing "Z":
// key: "z" code: "KeyZ"
// On German keyboard, pressing the same physical key:
// key: "y" code: "KeyZ" ← code is still KeyZ!
| Property | Returns | Use for |
|---|---|---|
event.key | The character ('a', 'Enter', 'Escape') | Text input, shortcuts by character |
event.code | Physical key ('KeyA', 'Enter', 'Escape') | Games, keyboard-layout-independent shortcuts |
Keyboard-Accessible Button
function AccessibleCustomButton({ onClick, children }) {
function handleKeyDown(event) {
// Space or Enter activates button
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault(); // Prevent scrolling on space
onClick();
}
}
return (
<div
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={handleKeyDown}
style={{ cursor: 'pointer', display: 'inline-block' }}
>
{children}
</div>
);
}
Global Keyboard Shortcuts Hook
function useKeyboardShortcut(key, callback, modifiers = {}) {
useEffect(() => {
function handleKeyDown(event) {
const matchesKey = event.key.toLowerCase() === key.toLowerCase();
const matchesMeta = !modifiers.meta || event.metaKey || event.ctrlKey;
const matchesShift = !modifiers.shift || event.shiftKey;
const matchesAlt = !modifiers.alt || event.altKey;
if (matchesKey && matchesMeta && matchesShift && matchesAlt) {
event.preventDefault();
callback(event);
}
}
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [key, callback, modifiers]);
}
// Usage
function App() {
useKeyboardShortcut('k', () => openCommandPalette(), { meta: true });
useKeyboardShortcut('Escape', () => closeModal());
useKeyboardShortcut('s', () => save(), { meta: true });
// ...
}
13. Focus and Blur Events
Basic Focus/Blur
function FocusDemo() {
const [isFocused, setIsFocused] = useState(false);
return (
<input
type="text"
placeholder="Focus me"
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
style={{
border: `2px solid ${isFocused ? '#0070f3' : '#ccc'}`,
outline: 'none',
padding: '8px 12px',
borderRadius: 4,
transition: 'border-color 0.2s',
}}
/>
);
}
Focus Within (Container Focus Tracking)
function FocusWithinDemo() {
const [hasFocusWithin, setHasFocusWithin] = useState(false);
return (
<div
onFocus={() => setHasFocusWithin(true)}
onBlur={(event) => {
// Check if focus moved OUTSIDE this container
if (!event.currentTarget.contains(event.relatedTarget)) {
setHasFocusWithin(false);
}
}}
style={{
padding: 20,
border: `2px solid ${hasFocusWithin ? '#0070f3' : '#eee'}`,
borderRadius: 8,
}}
>
<input type="text" placeholder="First name" />
<input type="text" placeholder="Last name" />
<input type="email" placeholder="Email" />
</div>
);
}
relatedTarget — Where Focus Came From / Went To
function RelatedTargetDemo() {
function handleBlur(event) {
console.log('Lost focus from:', event.target);
console.log('Focus moved to:', event.relatedTarget);
// If relatedTarget is null, focus left the window
if (!event.relatedTarget) {
console.log('User tabbed out of the browser');
}
}
return <input onBlur={handleBlur} />;
}
14. Touch Events for Mobile
Basic Touch Handling
function TouchDemo() {
const [touchPos, setTouchPos] = useState(null);
function handleTouchStart(event) {
const touch = event.touches[0];
setTouchPos({ x: touch.clientX, y: touch.clientY });
}
function handleTouchMove(event) {
const touch = event.touches[0];
setTouchPos({ x: touch.clientX, y: touch.clientY });
}
function handleTouchEnd() {
setTouchPos(null);
}
return (
<div
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{ width: 300, height: 300, background: '#f0f0f0', position: 'relative' }}
>
{touchPos && (
<div
style={{
position: 'absolute',
left: touchPos.x - 10,
top: touchPos.y - 10,
width: 20,
height: 20,
borderRadius: '50%',
background: 'blue',
}}
/>
)}
</div>
);
}
Swipe Detection
function useSwipe(onSwipeLeft, onSwipeRight, threshold = 50) {
const touchStartX = useRef(null);
function handleTouchStart(event) {
touchStartX.current = event.touches[0].clientX;
}
function handleTouchEnd(event) {
if (touchStartX.current === null) return;
const touchEndX = event.changedTouches[0].clientX;
const diff = touchStartX.current - touchEndX;
if (Math.abs(diff) > threshold) {
if (diff > 0) {
onSwipeLeft?.();
} else {
onSwipeRight?.();
}
}
touchStartX.current = null;
}
return { onTouchStart: handleTouchStart, onTouchEnd: handleTouchEnd };
}
// Usage
function Carousel() {
const [slide, setSlide] = useState(0);
const swipeHandlers = useSwipe(
() => setSlide(s => s + 1), // left swipe → next
() => setSlide(s => s - 1), // right swipe → prev
);
return <div {...swipeHandlers}>Slide {slide}</div>;
}
15. Event Delegation in React — You Don't Need It
In vanilla JS, event delegation is a performance optimization where you attach one listener to a parent instead of many listeners to children. React already does this for you.
Vanilla JS Delegation (Manual)
// Without delegation — BAD for 1000 items
document.querySelectorAll('.item').forEach(item => {
item.addEventListener('click', handleClick);
});
// With delegation — one listener
document.querySelector('.list').addEventListener('click', (event) => {
if (event.target.matches('.item')) {
handleClick(event);
}
});
React — Delegation is Built In
// React handles delegation internally
// You write this, React optimizes it
function ItemList({ items }) {
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
// React attaches ONE listener at the root, not 1000 on each <li>
You never need manual event delegation in React. Just write handlers normally and React handles it.
16. TypeScript and Events
Typing Event Handlers
// Method 1: Type the event parameter
function ClickHandler() {
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
console.log(event.clientX);
}
return <button onClick={handleClick}>Click</button>;
}
// Method 2: Type the handler function
function FormHandler() {
const handleSubmit: React.FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
};
return <form onSubmit={handleSubmit}>...</form>;
}
Common Event Types in TypeScript
// Mouse events
React.MouseEvent<HTMLButtonElement> // onClick, onDoubleClick
React.MouseEvent<HTMLDivElement> // onMouseEnter, onMouseLeave
// Keyboard events
React.KeyboardEvent<HTMLInputElement> // onKeyDown, onKeyUp
// Form events
React.FormEvent<HTMLFormElement> // onSubmit
React.ChangeEvent<HTMLInputElement> // onChange
React.ChangeEvent<HTMLSelectElement> // onChange for select
React.ChangeEvent<HTMLTextAreaElement> // onChange for textarea
// Focus events
React.FocusEvent<HTMLInputElement> // onFocus, onBlur
// Drag events
React.DragEvent<HTMLDivElement> // onDrag, onDrop
// Touch events
React.TouchEvent<HTMLDivElement> // onTouchStart, onTouchEnd
// Clipboard events
React.ClipboardEvent<HTMLInputElement> // onCopy, onPaste
// Scroll events
React.UIEvent<HTMLDivElement> // onScroll
Generic Reusable Handler
function useEventHandler<E extends HTMLElement>(
handler: (event: React.MouseEvent<E>) => void
) {
return useCallback(handler, [handler]);
}
17. Performance Considerations
Problem: New Functions on Every Render
function List({ items, onItemClick }) {
return (
<ul>
{items.map(item => (
// ⚠️ Creates a new function every render
<li key={item.id} onClick={() => onItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
Solution 1: useCallback for Stable References
function List({ items, onItemClick }) {
const handleClick = useCallback((id) => {
onItemClick(id);
}, [onItemClick]);
return (
<ul>
{items.map(item => (
<MemoizedItem key={item.id} item={item} onClick={handleClick} />
))}
</ul>
);
}
const MemoizedItem = React.memo(function Item({ item, onClick }) {
return <li onClick={() => onClick(item.id)}>{item.name}</li>;
});
Solution 2: Data Attributes (Zero Extra Functions)
function List({ items }) {
function handleClick(event) {
const id = event.currentTarget.dataset.id;
console.log('Clicked item:', id);
}
return (
<ul>
{items.map(item => (
<li key={item.id} data-id={item.id} onClick={handleClick}>
{item.name}
</li>
))}
</ul>
);
}
When Performance Actually Matters
| Scenario | Optimize? | Why |
|---|---|---|
| List < 100 items | No | Negligible overhead |
| List > 500 items | Maybe | Test first with profiler |
| Handler triggers heavy computation | Yes | useCallback + useMemo |
| Passing handler to React.memo child | Yes | Prevents unnecessary re-renders |
| Simple button click | No | Over-optimization is worse |
React Profiler: Open DevTools → Profiler tab → Record → Click around → See which renders were unnecessary.
18. Common Mistakes and Anti-Patterns
Mistake 1: Calling Instead of Passing
// ❌ WRONG — calls handleClick during render
<button onClick={handleClick()}>Click</button>
// ✅ CORRECT — passes function reference
<button onClick={handleClick}>Click</button>
// ✅ CORRECT — arrow wrapper for arguments
<button onClick={() => handleClick('arg')}>Click</button>
Mistake 2: Forgetting preventDefault on Forms
// ❌ WRONG — page reloads
function Form() {
function handleSubmit() {
console.log('Submitted'); // You'll never see this — page reloads
}
return <form onSubmit={handleSubmit}><button>Submit</button></form>;
}
// ✅ CORRECT
function Form() {
function handleSubmit(event) {
event.preventDefault();
console.log('Submitted');
}
return <form onSubmit={handleSubmit}><button>Submit</button></form>;
}
Mistake 3: Not Stopping Propagation in Nested Interactives
// ❌ WRONG — clicking delete opens card detail
function Card({ onOpen, onDelete }) {
return (
<div onClick={onOpen}>
<h3>Card Title</h3>
<button onClick={onDelete}>Delete</button> {/* Also triggers onOpen! */}
</div>
);
}
// ✅ CORRECT
function Card({ onOpen, onDelete }) {
function handleDelete(event) {
event.stopPropagation();
onDelete();
}
return (
<div onClick={onOpen}>
<h3>Card Title</h3>
<button onClick={handleDelete}>Delete</button>
</div>
);
}
Mistake 4: Async Event Properties
// ❌ WRONG — event properties may be null in async callback (React 16)
function AsyncBug() {
function handleClick(event) {
setTimeout(() => {
console.log(event.target); // May be null in React 16!
}, 100);
}
return <button onClick={handleClick}>Click</button>;
}
// ✅ CORRECT — extract values first
function AsyncFix() {
function handleClick(event) {
const target = event.target; // Extract synchronously
setTimeout(() => {
console.log(target); // Always works
}, 100);
}
return <button onClick={handleClick}>Click</button>;
}
// Note: React 17+ no longer pools events, so this is less of an issue.
// But extracting values is still good practice.
Mistake 5: Missing Event Argument
// ❌ WRONG — forgot event parameter
function WrongHandler() {
function handleSubmit() {
event.preventDefault(); // 'event' is undefined!
}
return <form onSubmit={handleSubmit}>...</form>;
}
// ✅ CORRECT — event is the first parameter
function CorrectHandler() {
function handleSubmit(event) {
event.preventDefault();
}
return <form onSubmit={handleSubmit}>...</form>;
}
19. Real-World Examples
Like Button with Animation
function LikeButton({ initialLiked = false, initialCount = 0 }) {
const [liked, setLiked] = useState(initialLiked);
const [count, setCount] = useState(initialCount);
const [animating, setAnimating] = useState(false);
function handleClick() {
setLiked(!liked);
setCount(prev => liked ? prev - 1 : prev + 1);
setAnimating(true);
setTimeout(() => setAnimating(false), 300);
}
return (
<button
onClick={handleClick}
style={{
background: 'none',
border: '1px solid #eee',
padding: '8px 16px',
borderRadius: 20,
cursor: 'pointer',
transform: animating ? 'scale(1.2)' : 'scale(1)',
transition: 'transform 0.3s ease',
}}
>
<span style={{ color: liked ? 'red' : '#999' }}>
{liked ? '❤️' : '🤍'}
</span>
{' '}{count}
</button>
);
}
Context Menu
function ContextMenu() {
const [menu, setMenu] = useState(null);
function handleContextMenu(event) {
event.preventDefault();
setMenu({ x: event.clientX, y: event.clientY });
}
function handleClick() {
setMenu(null); // Close menu on any click
}
useEffect(() => {
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, []);
return (
<div onContextMenu={handleContextMenu} style={{ height: 300, background: '#f5f5f5' }}>
<p>Right-click anywhere in this area</p>
{menu && (
<div
style={{
position: 'fixed',
left: menu.x,
top: menu.y,
background: 'white',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
borderRadius: 8,
padding: 8,
zIndex: 1000,
}}
>
<button onClick={() => console.log('Edit')}>✏️ Edit</button><br />
<button onClick={() => console.log('Copy')}>📋 Copy</button><br />
<button onClick={() => console.log('Delete')}>🗑️ Delete</button>
</div>
)}
</div>
);
}
Debounced Search Input
function DebouncedSearch() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const timeoutRef = useRef(null);
function handleChange(event) {
const value = event.target.value;
setQuery(value);
// Debounce: wait 300ms after user stops typing
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
if (value.trim()) {
fetchResults(value).then(setResults);
} else {
setResults([]);
}
}, 300);
}
// Cleanup on unmount
useEffect(() => {
return () => clearTimeout(timeoutRef.current);
}, []);
return (
<div>
<input
type="search"
value={query}
onChange={handleChange}
placeholder="Search..."
/>
<ul>
{results.map(r => <li key={r.id}>{r.name}</li>)}
</ul>
</div>
);
}
20. Key Takeaways
- React uses SyntheticEvents — cross-browser wrappers with the same API as native events
- Event names are camelCase in JSX —
onClicknotonclick - Pass function references, don't call them —
onClick={fn}notonClick={fn()} event.targetis what was clicked;event.currentTargetis where the handler livespreventDefault()stops browser defaults;stopPropagation()stops bubbling- React handles event delegation — you never need to manually delegate
- Name handlers
handleX, name propsonX - Functional components have no
thisproblem — one of the main reasons they won - Performance rarely matters for event handlers — profile before optimizing
- TypeScript event types follow the pattern
React.{EventType}<{HTMLElement}>
Explain-It Challenge
-
The Cocktail Party Explanation: Explain to a non-developer friend how React handles user clicks differently from a plain HTML button. Use the analogy of a receptionist at a company — all calls go through one person who routes them to the right department.
-
The Junior Developer Walkthrough: A junior developer's form keeps reloading the page when submitted. Walk them through debugging it — what's the cause, the fix, and the underlying concept they need to understand.
-
The Architecture Discussion: In a component hierarchy where a Card wraps a Button and both need click handlers, explain why
stopPropagation()is necessary and when you might NOT want to use it (hint: analytics, global close handlers).
Navigation: ← Overview · Next → Controlled Inputs