Event Handling Implementation Patterns
1. React SyntheticEvent preventDefault
| Feature | Syntax | Description | Use Case |
|---|---|---|---|
| SyntheticEvent | event: React.MouseEvent<T> |
Cross-browser wrapper around native events | Consistent event handling |
| preventDefault | event.preventDefault() |
Prevents default browser action | Form submission, link navigation |
| stopPropagation | event.stopPropagation() |
Stops event bubbling to parent elements | Nested clickable elements |
| currentTarget | event.currentTarget |
Element handler is attached to | Event delegation |
| target | event.target |
Element that triggered the event | Dynamic element access |
| nativeEvent | event.nativeEvent |
Access underlying browser event | Browser-specific features |
Example: React SyntheticEvent with type-safe handlers
import React from 'react';
// Form submission with preventDefault
function LoginForm() {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); // Prevent page reload
const formData = new FormData(event.currentTarget);
const email = formData.get('email') as string;
const password = formData.get('password') as string;
console.log({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Login</button>
</form>
);
}
// Click event with stopPropagation
function NestedButtons() {
const handleParentClick = () => {
console.log('Parent clicked');
};
const handleChildClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation(); // Prevent parent handler from firing
console.log('Child clicked');
};
return (
<div onClick={handleParentClick} style={{ padding: '20px', background: '#eee' }}>
<button onClick={handleChildClick}>
Click me (won't trigger parent)
</button>
</div>
);
}
// Input change with proper typing
function SearchInput() {
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
console.log('Search:', value);
};
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
event.preventDefault();
console.log('Search submitted');
}
};
return (
<input
type="text"
onChange={handleChange}
onKeyPress={handleKeyPress}
placeholder="Search..."
/>
);
}
// Mouse events with coordinates
function TrackMouse() {
const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
const { clientX, clientY, pageX, pageY } = event;
console.log('Client:', clientX, clientY);
console.log('Page:', pageX, pageY);
};
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
// Access native event for browser-specific features
console.log('Native event:', event.nativeEvent);
console.log('Button clicked:', event.button); // 0=left, 1=middle, 2=right
};
return (
<div
onMouseMove={handleMouseMove}
onClick={handleClick}
style={{ width: '300px', height: '300px', background: '#f0f0f0' }}
>
Move mouse here
</div>
);
}
// Focus events
function FocusExample() {
const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
event.target.style.borderColor = 'blue';
};
const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
event.target.style.borderColor = 'gray';
};
return (
<input
type="text"
onFocus={handleFocus}
onBlur={handleBlur}
placeholder="Focus me"
/>
);
}
2. Event Delegation querySelector addEventListener
| Method | Syntax | Description | Use Case |
|---|---|---|---|
| addEventListener | element.addEventListener('click', handler) |
Attaches event listener to element | DOM event handling |
| removeEventListener | element.removeEventListener('click', handler) |
Removes specific event listener | Cleanup, memory management |
| Event Delegation | parent.addEventListener('click', (e) => {}) |
Single listener on parent handles child events | Dynamic elements, performance |
| event.target | e.target.matches('.class') |
Identifies which child triggered event | Delegation filtering |
| capture phase | addEventListener('click', fn, true) |
Event fires during capture before bubble | Event interception |
| once option | addEventListener('click', fn, { once: true }) |
Listener auto-removes after first call | One-time events |
Example: Event delegation with dynamic elements
// Event delegation for dynamic list items
function setupDynamicList() {
const list = document.querySelector('#todo-list');
// Single event listener on parent handles all children
list.addEventListener('click', (event) => {
const target = event.target;
// Check if clicked element matches selector
if (target.matches('.delete-btn')) {
const todoItem = target.closest('.todo-item');
todoItem.remove();
}
if (target.matches('.checkbox')) {
const todoItem = target.closest('.todo-item');
todoItem.classList.toggle('completed');
}
if (target.matches('.edit-btn')) {
const todoItem = target.closest('.todo-item');
editTodo(todoItem);
}
});
// Add new items dynamically - no need for new listeners
function addTodo(text) {
const li = document.createElement('li');
li.className = 'todo-item';
li.innerHTML = `
<input type="checkbox" class="checkbox">
<span>${text}</span>
<button class="edit-btn">Edit</button>
<button class="delete-btn">Delete</button>
`;
list.appendChild(li);
}
}
// Event listener options
function advancedEventListening() {
const button = document.querySelector('#submit-btn');
// Once option - auto-removes after first call
button.addEventListener('click', handleClick, { once: true });
// Passive option - improves scroll performance
document.addEventListener('scroll', handleScroll, { passive: true });
// Capture phase - fires before bubble phase
document.addEventListener('click', captureHandler, true);
function handleClick(event) {
console.log('Button clicked once');
}
function handleScroll(event) {
// Cannot call preventDefault in passive listener
console.log('Scrolling...');
}
function captureHandler(event) {
console.log('Capture phase:', event.target);
}
}
// React hook for native event listeners
import { useEffect, useRef } from 'react';
function useEventListener(
eventName: string,
handler: (event: Event) => void,
element: HTMLElement | Window = window
) {
const savedHandler = useRef<(event: Event) => void>();
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const isSupported = element && element.addEventListener;
if (!isSupported) return;
const eventListener = (event: Event) => savedHandler.current?.(event);
element.addEventListener(eventName, eventListener);
return () => {
element.removeEventListener(eventName, eventListener);
};
}, [eventName, element]);
}
// Usage in React component
function Component() {
const divRef = useRef<HTMLDivElement>(null);
useEventListener('mousemove', (event) => {
console.log('Mouse position:', event.clientX, event.clientY);
}, divRef.current);
useEventListener('resize', () => {
console.log('Window resized');
}, window);
return <div ref={divRef}>Content</div>;
}
// Event delegation with closest()
document.querySelector('.container').addEventListener('click', (event) => {
// Find closest ancestor matching selector
const card = event.target.closest('.card');
if (card) {
console.log('Card clicked:', card.dataset.id);
}
});
3. Custom Events dispatch CustomEvent
| API | Syntax | Description | Use Case |
|---|---|---|---|
| CustomEvent | new CustomEvent('type', { detail }) |
Creates custom event with data payload | Component communication |
| detail | { detail: { data: value } } |
Payload data attached to event | Data passing |
| bubbles | { bubbles: true } |
Event propagates up DOM tree | Event bubbling |
| dispatchEvent | element.dispatchEvent(event) |
Triggers custom event on element | Event emission |
| composed | { composed: true } |
Event crosses shadow DOM boundary | Web Components |
Example: Custom events for component communication
// Creating and dispatching custom events
class NotificationSystem {
static show(message, type = 'info') {
const event = new CustomEvent('app:notification', {
detail: {
message,
type,
timestamp: Date.now(),
},
bubbles: true,
composed: true,
});
document.dispatchEvent(event);
}
}
// Listening to custom events
document.addEventListener('app:notification', (event) => {
const { message, type, timestamp } = event.detail;
console.log(`[${type}] ${message} at ${new Date(timestamp)}`);
showToast(message, type);
});
// Usage
NotificationSystem.show('User logged in', 'success');
NotificationSystem.show('Failed to load data', 'error');
// React hook for custom events
function useCustomEvent(eventName, handler) {
useEffect(() => {
const eventHandler = (event) => handler(event.detail);
document.addEventListener(eventName, eventHandler);
return () => document.removeEventListener(eventName, eventHandler);
}, [eventName, handler]);
}
// Component emitting custom event
function CartButton() {
const addToCart = (product) => {
const event = new CustomEvent('cart:add', {
detail: { product, quantity: 1 },
bubbles: true,
});
document.dispatchEvent(event);
};
return <button onClick={() => addToCart({ id: 1, name: 'Product' })}>Add to Cart</button>;
}
// Component listening to custom event
function CartCounter() {
const [count, setCount] = useState(0);
useCustomEvent('cart:add', (detail) => {
setCount(prev => prev + detail.quantity);
});
return <div>Cart: {count}</div>;
}
// Web Component with custom events
class UserCard extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<div class="card">
<h3>${this.getAttribute('name')}</h3>
<button id="follow-btn">Follow</button>
</div>
`;
this.querySelector('#follow-btn').addEventListener('click', () => {
const event = new CustomEvent('user:follow', {
detail: {
userId: this.getAttribute('user-id'),
userName: this.getAttribute('name'),
},
bubbles: true,
composed: true, // Cross shadow DOM
});
this.dispatchEvent(event);
});
}
}
customElements.define('user-card', UserCard);
// Listen to web component events
document.addEventListener('user:follow', (event) => {
console.log('Following user:', event.detail.userName);
});
// TypeScript typed custom events
interface AppEventMap {
'cart:add': CustomEvent<{ product: Product; quantity: number }>;
'user:login': CustomEvent<{ user: User }>;
'notification': CustomEvent<{ message: string; type: string }>;
}
function dispatchTypedEvent<K extends keyof AppEventMap>(
type: K,
detail: AppEventMap[K]['detail']
) {
const event = new CustomEvent(type, { detail, bubbles: true });
document.dispatchEvent(event);
}
// Type-safe usage
dispatchTypedEvent('cart:add', { product: myProduct, quantity: 2 });
4. Debounce Throttle Lodash useDebounce
| Technique | Behavior | Description | Use Case |
|---|---|---|---|
| Debounce | Delays execution until pause in events | Waits for event stream to stop, then executes once | Search input, window resize |
| Throttle | Limits execution rate to interval | Executes at most once per time period | Scroll, mouse move, animation |
| lodash.debounce | debounce(fn, delay, options) |
Debounce with leading/trailing edge control | Production-ready utility |
| lodash.throttle | throttle(fn, interval, options) |
Throttle with leading/trailing edge control | Rate limiting |
| useDebounce hook | const debounced = useDebounce(value, delay) |
React hook for debounced values | React state debouncing |
Example: Debounce and throttle implementations with React hooks
// Pure JavaScript debounce implementation
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// Pure JavaScript throttle implementation
function throttle(func, interval) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= interval) {
lastCall = now;
func.apply(this, args);
}
};
}
// Debounced search input
const searchInput = document.querySelector('#search');
const debouncedSearch = debounce((query) => {
console.log('Searching for:', query);
fetchResults(query);
}, 500);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
// Throttled scroll handler
const handleScroll = throttle(() => {
console.log('Scroll position:', window.scrollY);
updateScrollPosition();
}, 100);
window.addEventListener('scroll', handleScroll);
// React useDebounce hook
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// React useThrottle hook
function useThrottle<T>(value: T, interval: number): T {
const [throttledValue, setThrottledValue] = useState(value);
const lastUpdated = useRef(Date.now());
useEffect(() => {
const now = Date.now();
const timeSinceLastUpdate = now - lastUpdated.current;
if (timeSinceLastUpdate >= interval) {
lastUpdated.current = now;
setThrottledValue(value);
} else {
const timeoutId = setTimeout(() => {
lastUpdated.current = Date.now();
setThrottledValue(value);
}, interval - timeSinceLastUpdate);
return () => clearTimeout(timeoutId);
}
}, [value, interval]);
return throttledValue;
}
// Debounced search component
function SearchBox() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
console.log('Searching:', debouncedSearchTerm);
fetchSearchResults(debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}
// Throttled scroll component
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
const throttledScrollY = useThrottle(scrollY, 100);
useEffect(() => {
const handleScroll = () => setScrollY(window.scrollY);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return <div>Scroll Position: {throttledScrollY}px</div>;
}
// Using lodash debounce/throttle
import { debounce, throttle } from 'lodash';
function SearchWithLodash() {
const debouncedSearch = useMemo(
() => debounce((query) => {
console.log('Searching:', query);
}, 500, {
leading: false,
trailing: true,
}),
[]
);
useEffect(() => {
return () => debouncedSearch.cancel(); // Cleanup
}, [debouncedSearch]);
return (
<input
onChange={(e) => debouncedSearch(e.target.value)}
placeholder="Search..."
/>
);
}
// Advanced: Debounce with abort controller
function useDebouncedCallback(callback, delay) {
const callbackRef = useRef(callback);
const timeoutRef = useRef();
const abortControllerRef = useRef();
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
return useCallback((...args) => {
// Cancel previous request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
abortControllerRef.current = new AbortController();
callbackRef.current(...args, abortControllerRef.current.signal);
}, delay);
}, [delay]);
}
5. Keyboard Navigation Tab Index Focus
| Property/Method | Syntax | Description | Use Case |
|---|---|---|---|
| tabIndex | tabIndex={0} |
Makes element keyboard focusable in DOM order | Custom interactive elements |
| tabIndex -1 | tabIndex={-1} |
Focusable programmatically but not via Tab | Modal focus management |
| focus() | element.focus() |
Programmatically sets focus to element | Auto-focus input |
| onKeyDown | onKeyDown={(e) => {}} |
Handles keyboard key press events | Keyboard shortcuts |
| roving tabindex | One item tabIndex={0}, others -1 | Single Tab stop for component group | Toolbars, menus, lists |
| aria-activedescendant | aria-activedescendant="id" |
Indicates active descendant without focus move | Listbox, combobox |
Example: Keyboard navigation with roving tabindex
// Simple keyboard navigation
function KeyboardShortcuts() {
const handleKeyDown = (event: React.KeyboardEvent) => {
// Ctrl/Cmd + S = Save
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault();
handleSave();
}
// Escape = Close modal
if (event.key === 'Escape') {
handleClose();
}
// Arrow keys = Navigation
if (event.key === 'ArrowDown') {
event.preventDefault();
navigateDown();
}
};
return (
<div onKeyDown={handleKeyDown} tabIndex={0}>
Press Ctrl+S to save
</div>
);
}
// Roving tabindex implementation
function Toolbar() {
const [focusedIndex, setFocusedIndex] = useState(0);
const buttonsRef = useRef<(HTMLButtonElement | null)[]>([]);
const buttons = ['Cut', 'Copy', 'Paste', 'Undo', 'Redo'];
const handleKeyDown = (event: React.KeyboardEvent, index: number) => {
let newIndex = index;
switch (event.key) {
case 'ArrowRight':
event.preventDefault();
newIndex = Math.min(index + 1, buttons.length - 1);
break;
case 'ArrowLeft':
event.preventDefault();
newIndex = Math.max(index - 1, 0);
break;
case 'Home':
event.preventDefault();
newIndex = 0;
break;
case 'End':
event.preventDefault();
newIndex = buttons.length - 1;
break;
default:
return;
}
setFocusedIndex(newIndex);
buttonsRef.current[newIndex]?.focus();
};
return (
<div role="toolbar" aria-label="Text formatting">
{buttons.map((label, index) => (
<button
key={label}
ref={(el) => (buttonsRef.current[index] = el)}
tabIndex={index === focusedIndex ? 0 : -1}
onKeyDown={(e) => handleKeyDown(e, index)}
onClick={() => console.log(`${label} clicked`)}
>
{label}
</button>
))}
</div>
);
}
// Focus trap for modal
function useFocusTrap(ref: React.RefObject<HTMLElement>) {
useEffect(() => {
const element = ref.current;
if (!element) return;
const focusableElements = element.querySelectorAll(
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0] as HTMLElement;
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
const handleTabKey = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
};
element.addEventListener('keydown', handleTabKey);
firstElement?.focus();
return () => element.removeEventListener('keydown', handleTabKey);
}, [ref]);
}
// Modal with focus management
function Modal({ isOpen, onClose, children }) {
const modalRef = useRef<HTMLDivElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(null);
useFocusTrap(modalRef);
useEffect(() => {
if (isOpen) {
previousFocusRef.current = document.activeElement as HTMLElement;
} else if (previousFocusRef.current) {
previousFocusRef.current.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
onKeyDown={(e) => e.key === 'Escape' && onClose()}
>
{children}
<button onClick={onClose}>Close</button>
</div>
);
}
// Custom dropdown with keyboard navigation
function Dropdown({ options }: { options: string[] }) {
const [isOpen, setIsOpen] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(-1);
const listRef = useRef<HTMLUListElement>(null);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (!isOpen) {
if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
e.preventDefault();
setIsOpen(true);
setSelectedIndex(0);
}
return;
}
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setSelectedIndex((prev) => Math.min(prev + 1, options.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex((prev) => Math.max(prev - 1, 0));
break;
case 'Enter':
case ' ':
e.preventDefault();
console.log('Selected:', options[selectedIndex]);
setIsOpen(false);
break;
case 'Escape':
e.preventDefault();
setIsOpen(false);
break;
}
};
return (
<div>
<button
onKeyDown={handleKeyDown}
onClick={() => setIsOpen(!isOpen)}
aria-haspopup="listbox"
aria-expanded={isOpen}
>
Select option
</button>
{isOpen && (
<ul ref={listRef} role="listbox">
{options.map((option, index) => (
<li
key={option}
role="option"
aria-selected={index === selectedIndex}
style={{
background: index === selectedIndex ? '#e0e0e0' : 'transparent',
}}
>
{option}
</li>
))}
</ul>
)}
</div>
);
}
6. Touch Events Mobile Gesture Handling
| Event | Trigger | Description | Use Case |
|---|---|---|---|
| touchstart | Finger touches screen | Fires when touch begins | Gesture initiation |
| touchmove | Finger moves on screen | Fires repeatedly during touch movement | Drag, swipe tracking |
| touchend | Finger leaves screen | Fires when touch ends | Gesture completion |
| touches | event.touches |
Array of all current touch points | Multi-touch gestures |
| changedTouches | event.changedTouches |
Touches that changed in this event | Specific touch handling |
| Pointer Events | onPointerDown/Move/Up |
Unified API for mouse, touch, pen | Cross-device input |
Example: Touch gestures with swipe and pinch detection
// Swipe gesture detection
function useSwipe(onSwipeLeft, onSwipeRight, threshold = 50) {
const touchStart = useRef<{ x: number; y: number } | null>(null);
const handleTouchStart = (e: React.TouchEvent) => {
touchStart.current = {
x: e.touches[0].clientX,
y: e.touches[0].clientY,
};
};
const handleTouchEnd = (e: React.TouchEvent) => {
if (!touchStart.current) return;
const touchEnd = {
x: e.changedTouches[0].clientX,
y: e.changedTouches[0].clientY,
};
const deltaX = touchEnd.x - touchStart.current.x;
const deltaY = Math.abs(touchEnd.y - touchStart.current.y);
// Horizontal swipe (deltaX > deltaY)
if (Math.abs(deltaX) > threshold && Math.abs(deltaX) > deltaY) {
if (deltaX > 0) {
onSwipeRight?.();
} else {
onSwipeLeft?.();
}
}
touchStart.current = null;
};
return { handleTouchStart, handleTouchEnd };
}
// Swipeable component
function SwipeableCard() {
const { handleTouchStart, handleTouchEnd } = useSwipe(
() => console.log('Swiped left'),
() => console.log('Swiped right')
);
return (
<div
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
style={{
width: '300px',
height: '200px',
background: '#f0f0f0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
Swipe me left or right
</div>
);
}
// Draggable element with touch
function DraggableElement() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const startPos = useRef({ x: 0, y: 0 });
const currentPos = useRef({ x: 0, y: 0 });
const handleTouchStart = (e: React.TouchEvent) => {
setIsDragging(true);
startPos.current = {
x: e.touches[0].clientX - position.x,
y: e.touches[0].clientY - position.y,
};
};
const handleTouchMove = (e: React.TouchEvent) => {
if (!isDragging) return;
currentPos.current = {
x: e.touches[0].clientX - startPos.current.x,
y: e.touches[0].clientY - startPos.current.y,
};
setPosition(currentPos.current);
};
const handleTouchEnd = () => {
setIsDragging(false);
};
return (
<div
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{
position: 'absolute',
left: `${position.x}px`,
top: `${position.y}px`,
width: '100px',
height: '100px',
background: '#4CAF50',
cursor: isDragging ? 'grabbing' : 'grab',
touchAction: 'none', // Prevent default touch behaviors
}}
>
Drag me
</div>
);
}
// Pinch-to-zoom gesture
function usePinchZoom() {
const [scale, setScale] = useState(1);
const initialDistance = useRef<number | null>(null);
const initialScale = useRef(1);
const getDistance = (touch1: React.Touch, touch2: React.Touch) => {
const dx = touch1.clientX - touch2.clientX;
const dy = touch1.clientY - touch2.clientY;
return Math.sqrt(dx * dx + dy * dy);
};
const handleTouchStart = (e: React.TouchEvent) => {
if (e.touches.length === 2) {
initialDistance.current = getDistance(e.touches[0], e.touches[1]);
initialScale.current = scale;
}
};
const handleTouchMove = (e: React.TouchEvent) => {
if (e.touches.length === 2 && initialDistance.current) {
const currentDistance = getDistance(e.touches[0], e.touches[1]);
const newScale = (currentDistance / initialDistance.current) * initialScale.current;
setScale(Math.min(Math.max(newScale, 0.5), 3)); // Limit scale 0.5x to 3x
}
};
const handleTouchEnd = () => {
initialDistance.current = null;
};
return { scale, handleTouchStart, handleTouchMove, handleTouchEnd };
}
// Zoomable image
function ZoomableImage({ src }: { src: string }) {
const { scale, handleTouchStart, handleTouchMove, handleTouchEnd } = usePinchZoom();
return (
<div
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{
overflow: 'hidden',
touchAction: 'none',
}}
>
<img
src={src}
alt="Zoomable"
style={{
transform: `scale(${scale})`,
transformOrigin: 'center center',
transition: 'transform 0.1s',
}}
/>
</div>
);
}
// Unified pointer events (mouse + touch + pen)
function UnifiedPointerHandler() {
const [isPressed, setIsPressed] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 0 });
const handlePointerDown = (e: React.PointerEvent) => {
setIsPressed(true);
(e.target as HTMLElement).setPointerCapture(e.pointerId);
};
const handlePointerMove = (e: React.PointerEvent) => {
if (isPressed) {
setPosition({ x: e.clientX, y: e.clientY });
}
};
const handlePointerUp = (e: React.PointerEvent) => {
setIsPressed(false);
(e.target as HTMLElement).releasePointerCapture(e.pointerId);
};
return (
<div
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
style={{
width: '100%',
height: '400px',
background: '#f0f0f0',
position: 'relative',
touchAction: 'none',
}}
>
<div
style={{
position: 'absolute',
left: `${position.x}px`,
top: `${position.y}px`,
width: '20px',
height: '20px',
background: isPressed ? 'red' : 'blue',
borderRadius: '50%',
transform: 'translate(-50%, -50%)',
}}
/>
</div>
);
}
Event Handling Best Practices
- SyntheticEvent - React's cross-browser event wrapper, use preventDefault() for form submissions
- Event Delegation - Single listener on parent for dynamic children, improves performance
- Custom Events - Enable decoupled component communication via browser's event system
- Debounce - For search, resize, input validation (wait for pause)
- Throttle - For scroll, mouse move, animations (limit execution rate)
- Keyboard Navigation - Roving tabindex for toolbars, arrow keys for lists, focus trap for modals
- Touch Events - Use pointer events for unified mouse/touch/pen handling, prevent default behaviors with touchAction