Event Handling and User Interactions
1. Event Handler Syntax and Event Objects
| Syntax | Pattern | Description | Use Case |
|---|---|---|---|
| Inline Arrow | onClick={() => handler()} |
Arrow function in JSX | Pass arguments, simple calls |
| Function Reference | onClick={handler} |
Direct function reference | Better performance, no args |
| Event Object | (e) => handler(e) |
Access event properties | Get target value, prevent default |
| With Parameters | onClick={(e) => handler(id, e)} |
Pass custom args with event | Item actions, list operations |
| Bind Method | onClick={this.handler.bind(this, id)} |
Class component binding | Legacy class components |
| Prevent Default | e.preventDefault() |
Stop default browser action | Forms, links, context menu |
| Stop Propagation | e.stopPropagation() |
Stop event bubbling | Nested clickable elements |
Example: Event handler patterns
// Basic event handlers
const EventHandlers = () => {
const [count, setCount] = useState(0);
// Simple handler
const handleClick = () => {
setCount(c => c + 1);
};
// Handler with event object
const handleChange = (e) => {
console.log('Input value:', e.target.value);
};
// Handler with custom parameters
const handleDelete = (id) => {
console.log('Delete item:', id);
};
// Handler with both custom params and event
const handleEdit = (id, e) => {
e.stopPropagation(); // Stop bubbling
console.log('Edit item:', id);
};
return (
<div>
{/* Function reference (preferred for simple cases) */}
<button onClick={handleClick}>Count: {count}</button>
{/* Inline arrow function (needed for params) */}
<button onClick={() => handleDelete(123)}>Delete</button>
{/* With event and params */}
<button onClick={(e) => handleEdit(456, e)}>Edit</button>
{/* Event object access */}
<input onChange={handleChange} />
</div>
);
};
// Prevent default behavior
const FormWithLink = () => {
const handleSubmit = (e) => {
e.preventDefault(); // Don't reload page
console.log('Form submitted');
};
const handleLinkClick = (e) => {
e.preventDefault(); // Don't navigate
console.log('Handle navigation in React');
};
return (
<div>
<form onSubmit={handleSubmit}>
<input type="text" />
<button type="submit">Submit</button>
</form>
<a href="/page" onClick={handleLinkClick}>
Custom navigation
</a>
</div>
);
};
// Stop propagation for nested elements
const NestedClickables = () => {
const handleParentClick = () => {
console.log('Parent clicked');
};
const handleChildClick = (e) => {
e.stopPropagation(); // Don't trigger parent
console.log('Child clicked');
};
return (
<div onClick={handleParentClick} style={{ padding: '2rem', border: '1px solid' }}>
Parent
<button onClick={handleChildClick}>
Child (won't trigger parent)
</button>
</div>
);
};
// Event object properties
const EventProperties = () => {
const handleEvent = (e) => {
console.log('Event type:', e.type);
console.log('Target element:', e.target);
console.log('Current target:', e.currentTarget);
console.log('Key pressed:', e.key);
console.log('Mouse position:', e.clientX, e.clientY);
console.log('Modifier keys:', {
ctrl: e.ctrlKey,
shift: e.shiftKey,
alt: e.altKey,
meta: e.metaKey
});
};
return (
<div>
<button onClick={handleEvent}>Click me</button>
<input onKeyDown={handleEvent} />
</div>
);
};
Note: Use function references when possible for better performance. Use inline arrow functions
only
when you need to pass arguments or access closure variables.
2. Synthetic Events and Cross-browser Compatibility
| Feature | Description | Benefits | Important Notes |
|---|---|---|---|
| Synthetic Events | React's wrapper around native events | Cross-browser consistency | Same API across all browsers |
| Event Pooling (Legacy) | Events reused for performance | Memory efficiency | Removed in React 17+ |
| nativeEvent | e.nativeEvent access |
Access underlying browser event | Use only when necessary |
| Event Delegation | Events attached to root (React 16) | Performance, memory efficiency | Changed in React 17 (root container) |
| Passive Events | Non-blocking scroll/touch events | Better scroll performance | Can't preventDefault() on passive |
Example: Synthetic events and cross-browser handling
// Synthetic event properties
const SyntheticEventDemo = () => {
const handleClick = (e) => {
// Synthetic event properties (same across browsers)
console.log('Synthetic event:', e);
console.log('Type:', e.type);
console.log('Target:', e.target);
console.log('Timestamp:', e.timeStamp);
// Access native browser event if needed
console.log('Native event:', e.nativeEvent);
// Common methods work consistently
e.preventDefault();
e.stopPropagation();
};
return <button onClick={handleClick}>Click</button>;
};
// No need to worry about event pooling in React 17+
const ModernEventHandling = () => {
const handleClick = (e) => {
// In React 17+, can access event properties asynchronously
setTimeout(() => {
console.log('Event type:', e.type); // Works!
console.log('Target:', e.target); // Works!
}, 1000);
};
return <button onClick={handleClick}>Click</button>;
};
// Legacy React (<17) required persist() for async access
const LegacyAsyncEventAccess = () => {
const handleClick = (e) => {
e.persist(); // No longer needed in React 17+
setTimeout(() => {
console.log('Event type:', e.type);
}, 1000);
};
return <button onClick={handleClick}>Click</button>;
};
// Native event access for special cases
const NativeEventAccess = () => {
const handleWheel = (e) => {
// Synthetic event
console.log('Delta Y:', e.deltaY);
// Native event for browser-specific features
const nativeEvent = e.nativeEvent;
console.log('Native wheel event:', nativeEvent);
};
return (
<div
onWheel={handleWheel}
style={{ height: '200px', overflow: 'auto' }}
>
Scroll me
</div>
);
};
// Cross-browser event handling
const CrossBrowserEvents = () => {
const handleInput = (e) => {
// Works consistently across all browsers
const value = e.target.value;
console.log('Input value:', value);
};
const handlePaste = (e) => {
// Clipboard access works consistently
const pastedText = e.clipboardData.getData('text');
console.log('Pasted:', pastedText);
};
const handleDrag = (e) => {
// Drag events normalized
console.log('Dragging:', e.dataTransfer);
};
return (
<div>
<input onChange={handleInput} onPaste={handlePaste} />
<div draggable onDrag={handleDrag}>Drag me</div>
</div>
);
};
// Event delegation (automatic in React)
const EventDelegationExample = () => {
// React automatically delegates events to root
// No need to manually attach/remove listeners
const handleClick = (id) => {
console.log('Clicked item:', id);
};
return (
<ul>
{[1, 2, 3, 4, 5].map(id => (
<li key={id} onClick={() => handleClick(id)}>
Item {id}
</li>
))}
</ul>
);
// React attaches ONE listener at root, not 5 listeners
};
Note: React's synthetic events provide consistent behavior across browsers. Event pooling was
removed
in React 17+, so you can safely access event properties asynchronously without calling
persist().
3. Event Handler Binding and Performance
| Pattern | Performance | When to Use | Example |
|---|---|---|---|
| Inline Arrow | Creates new function each render | Simple cases, rare renders | onClick={() => handler()} |
| useCallback | Memoized function reference | Optimize child re-renders | const cb = useCallback(() => {}, []) |
| Function Reference | Best - stable reference | No arguments needed | onClick={handler} |
| Closure Factory | Creates function per item | List items with IDs | const makeHandler = (id) => () => {} |
| Data Attributes | Single handler, read from event | Lists with simple actions | data-id={id} then read in handler |
Example: Event handler optimization
// ❌ Bad: Inline arrow in child with memo
const Child = memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>Click</button>;
});
const BadParent = () => {
const [count, setCount] = useState(0);
return (
<div>
{/* Child re-renders every time because new function */}
<Child onClick={() => console.log('clicked')} />
<button onClick={() => setCount(c => c + 1)}>{count}</button>
</div>
);
};
// ✓ Good: useCallback for stable reference
const GoodParent = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<div>
{/* Child won't re-render unnecessarily */}
<Child onClick={handleClick} />
<button onClick={() => setCount(c => c + 1)}>{count}</button>
</div>
);
};
// ❌ Bad: Creating new function per list item
const BadList = () => {
const [items, setItems] = useState([1, 2, 3, 4, 5]);
const handleDelete = (id) => {
setItems(items => items.filter(i => i !== id));
};
return (
<ul>
{items.map(item => (
<li key={item}>
{item}
{/* New function created for each item */}
<button onClick={() => handleDelete(item)}>Delete</button>
</li>
))}
</ul>
);
};
// ✓ Better: Data attributes pattern
const BetterList = () => {
const [items, setItems] = useState([1, 2, 3, 4, 5]);
const handleDelete = (e) => {
const id = parseInt(e.currentTarget.dataset.id);
setItems(items => items.filter(i => i !== id));
};
return (
<ul>
{items.map(item => (
<li key={item}>
{item}
{/* Single handler reference */}
<button data-id={item} onClick={handleDelete}>Delete</button>
</li>
))}
</ul>
);
};
// ✓ Good: Memoized list items
const ListItem = memo(({ item, onDelete }) => {
return (
<li>
{item}
<button onClick={onDelete}>Delete</button>
</li>
);
});
const OptimizedList = () => {
const [items, setItems] = useState([1, 2, 3, 4, 5]);
const handleDelete = useCallback((id) => {
setItems(items => items.filter(i => i !== id));
}, []);
return (
<ul>
{items.map(item => (
<ListItem
key={item}
item={item}
onDelete={() => handleDelete(item)}
/>
))}
</ul>
);
};
// Performance optimization decision guide
const PerformanceGuide = () => {
// Simple component, no memo on children → inline is fine
const simple = () => (
<button onClick={() => console.log('click')}>Click</button>
);
// Memo'ed children → use useCallback
const [count, setCount] = useState(0);
const optimized = useCallback(() => {
console.log('click');
}, []);
// Large lists → use data attributes or memoized items
// Rare updates → inline is fine
// Frequent updates → optimize
};
Warning: Don't prematurely optimize! Inline arrow functions are fine for most cases. Only use
useCallback when passing handlers to memoized children or in large lists with frequent updates.
4. Custom Event Handlers and Event Composition
| Pattern | Purpose | Implementation | Use Case |
|---|---|---|---|
| Event Composition | Chain multiple handlers | Call multiple functions in sequence | Logging + action |
| Higher-Order Handlers | Create handlers with common logic | Function that returns handler | Tracking, validation |
| Custom Events | Component-specific events | Custom event callbacks via props | onSuccess, onError callbacks |
| Event Middlewares | Transform or validate events | Wrapper function around handler | Auth checks, rate limiting |
| Debounced/Throttled | Control event frequency | useDebounce/useThrottle hooks | Search input, scroll, resize |
Example: Custom event handlers
// Event composition
const EventComposition = () => {
const logEvent = (eventName) => {
console.log(\`Event: \${eventName}\`);
};
const trackEvent = (eventName) => {
analytics.track(eventName);
};
const handleClick = () => {
console.log('Button clicked');
};
// Compose multiple handlers
const composedHandler = (e) => {
logEvent('button-click');
trackEvent('button-click');
handleClick();
};
return <button onClick={composedHandler}>Click</button>;
};
// Higher-order handler
const withTracking = (handler, eventName) => {
return (e) => {
analytics.track(eventName);
handler(e);
};
};
const withValidation = (handler, validate) => {
return (e) => {
if (validate(e)) {
handler(e);
}
};
};
const TrackedButton = () => {
const handleClick = () => {
console.log('Clicked');
};
const trackedHandler = withTracking(handleClick, 'button-clicked');
return <button onClick={trackedHandler}>Click</button>;
};
// Custom events via props
const FileUploader = ({ onUploadStart, onUploadSuccess, onUploadError }) => {
const [uploading, setUploading] = useState(false);
const handleUpload = async (file) => {
setUploading(true);
onUploadStart?.();
try {
const result = await uploadFile(file);
onUploadSuccess?.(result);
} catch (error) {
onUploadError?.(error);
} finally {
setUploading(false);
}
};
return (
<input
type="file"
onChange={(e) => handleUpload(e.target.files[0])}
disabled={uploading}
/>
);
};
// Usage
const App = () => (
<FileUploader
onUploadStart={() => console.log('Starting...')}
onUploadSuccess={(data) => console.log('Success:', data)}
onUploadError={(err) => console.error('Error:', err)}
/>
);
// Debounced event handler
const useDebounce = (callback, delay) => {
const timeoutRef = useRef(null);
return useCallback((...args) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callback(...args);
}, delay);
}, [callback, delay]);
};
const SearchInput = () => {
const [query, setQuery] = useState('');
const performSearch = (searchTerm) => {
console.log('Searching for:', searchTerm);
// API call here
};
const debouncedSearch = useDebounce(performSearch, 500);
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value);
};
return <input value={query} onChange={handleChange} />;
};
// Throttled event handler
const useThrottle = (callback, delay) => {
const lastRun = useRef(Date.now());
return useCallback((...args) => {
const now = Date.now();
if (now - lastRun.current >= delay) {
callback(...args);
lastRun.current = now;
}
}, [callback, delay]);
};
const ScrollTracker = () => {
const trackScroll = (scrollY) => {
console.log('Scroll position:', scrollY);
};
const throttledTrack = useThrottle(() => {
trackScroll(window.scrollY);
}, 1000);
useEffect(() => {
window.addEventListener('scroll', throttledTrack);
return () => window.removeEventListener('scroll', throttledTrack);
}, [throttledTrack]);
return <div>Scroll the page</div>;
};
// Event middleware pattern
const withAuth = (handler) => {
return (e) => {
const isAuthenticated = checkAuth();
if (!isAuthenticated) {
alert('Please log in');
return;
}
handler(e);
};
};
const ProtectedButton = () => {
const handleAction = () => {
console.log('Performing protected action');
};
return (
<button onClick={withAuth(handleAction)}>
Protected Action
</button>
);
};
5. Keyboard and Mouse Event Patterns
| Event Type | Event Name | Common Properties | Use Case |
|---|---|---|---|
| Keyboard | onKeyDown, onKeyUp, onKeyPress | key, code, keyCode, ctrlKey, shiftKey | Shortcuts, form navigation, games |
| Mouse Click | onClick, onDoubleClick, onContextMenu | button, clientX, clientY | Actions, menus, selection |
| Mouse Movement | onMouseMove, onMouseEnter, onMouseLeave | clientX, clientY, movementX, movementY | Tooltips, drag, hover effects |
| Mouse Drag | onMouseDown, onMouseMove, onMouseUp | Track position changes | Draggable elements, drawing |
| Wheel | onWheel | deltaX, deltaY, deltaZ | Custom scrolling, zoom |
Example: Keyboard and mouse events
// Keyboard shortcuts
const KeyboardShortcuts = () => {
const handleKeyDown = (e) => {
// Check for specific key
if (e.key === 'Enter') {
console.log('Enter pressed');
}
// Keyboard shortcuts with modifiers
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
console.log('Save (Ctrl+S)');
}
if (e.ctrlKey && e.shiftKey && e.key === 'P') {
e.preventDefault();
console.log('Open command palette (Ctrl+Shift+P)');
}
// Arrow keys
switch (e.key) {
case 'ArrowUp':
console.log('Up');
break;
case 'ArrowDown':
console.log('Down');
break;
case 'ArrowLeft':
console.log('Left');
break;
case 'ArrowRight':
console.log('Right');
break;
}
};
return (
<div onKeyDown={handleKeyDown} tabIndex={0}>
Press keys (Ctrl+S, Arrows, Enter)
</div>
);
};
// Key codes reference
const KeyboardReference = () => {
const handleKeyDown = (e) => {
console.log('Key:', e.key); // 'a', 'Enter', 'ArrowUp'
console.log('Code:', e.code); // 'KeyA', 'Enter', 'ArrowUp'
console.log('KeyCode:', e.keyCode); // 65, 13, 38 (deprecated)
console.log('Modifiers:', {
ctrl: e.ctrlKey,
shift: e.shiftKey,
alt: e.altKey,
meta: e.metaKey // Cmd on Mac, Win on Windows
});
};
return <input onKeyDown={handleKeyDown} />;
};
// Mouse events
const MouseEvents = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isHovering, setIsHovering] = useState(false);
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
const handleClick = (e) => {
console.log('Click at:', e.clientX, e.clientY);
console.log('Button:', e.button); // 0=left, 1=middle, 2=right
};
const handleDoubleClick = () => {
console.log('Double clicked');
};
const handleContextMenu = (e) => {
e.preventDefault(); // Prevent default context menu
console.log('Right clicked');
};
return (
<div
onMouseMove={handleMouseMove}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
onContextMenu={handleContextMenu}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
style={{
width: '100%',
height: '300px',
border: '1px solid',
backgroundColor: isHovering ? '#f0f0f0' : 'white'
}}
>
Mouse position: {position.x}, {position.y}
{isHovering && ' (Hovering)'}
</div>
);
};
// Draggable element
const DraggableBox = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
const handleMouseDown = (e) => {
setIsDragging(true);
setDragStart({
x: e.clientX - position.x,
y: e.clientY - position.y
});
};
const handleMouseMove = (e) => {
if (isDragging) {
setPosition({
x: e.clientX - dragStart.x,
y: e.clientY - dragStart.y
});
}
};
const handleMouseUp = () => {
setIsDragging(false);
};
useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
};
}
}, [isDragging, dragStart]);
return (
<div
onMouseDown={handleMouseDown}
style={{
position: 'absolute',
left: position.x,
top: position.y,
width: '100px',
height: '100px',
backgroundColor: 'blue',
cursor: isDragging ? 'grabbing' : 'grab'
}}
>
Drag me
</div>
);
};
// Hover tooltip
const HoverTooltip = ({ children, tooltip }) => {
const [show, setShow] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseEnter = (e) => {
setShow(true);
setPosition({ x: e.clientX, y: e.clientY });
};
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<>
<span
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={() => setShow(false)}
>
{children}
</span>
{show && (
<div
style={{
position: 'fixed',
left: position.x + 10,
top: position.y + 10,
background: 'black',
color: 'white',
padding: '5px',
borderRadius: '3px'
}}
>
{tooltip}
</div>
)}
</>
);
};
6. Touch Events and Mobile Interaction Handling
| Event | Description | Properties | Use Case |
|---|---|---|---|
| onTouchStart | Touch begins | touches, targetTouches, changedTouches | Detect touch, start gesture |
| onTouchMove | Touch moves | Touch coordinates, track movement | Swipe, drag, pan |
| onTouchEnd | Touch ends | Remaining touches | Complete gesture, trigger action |
| onTouchCancel | Touch interrupted | System interruption | Reset gesture state |
| Multi-touch | Multiple fingers | touches array | Pinch zoom, rotate |
| Gestures | Swipe, pinch, rotate | Calculate from touch positions | Mobile interactions |
Example: Touch events and mobile gestures
// Basic touch events
const TouchDemo = () => {
const [touchInfo, setTouchInfo] = useState('');
const handleTouchStart = (e) => {
const touch = e.touches[0];
setTouchInfo(\`Touch started at \${touch.clientX}, \${touch.clientY}\`);
};
const handleTouchMove = (e) => {
const touch = e.touches[0];
setTouchInfo(\`Moving at \${touch.clientX}, \${touch.clientY}\`);
};
const handleTouchEnd = () => {
setTouchInfo('Touch ended');
};
return (
<div
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{
width: '100%',
height: '200px',
border: '1px solid',
touchAction: 'none' // Prevent default touch behavior
}}
>
{touchInfo || 'Touch here'}
</div>
);
};
// Swipe detection
const SwipeDetector = ({ onSwipeLeft, onSwipeRight, onSwipeUp, onSwipeDown }) => {
const [touchStart, setTouchStart] = useState(null);
const handleTouchStart = (e) => {
const touch = e.touches[0];
setTouchStart({ x: touch.clientX, y: touch.clientY });
};
const handleTouchEnd = (e) => {
if (!touchStart) return;
const touch = e.changedTouches[0];
const deltaX = touch.clientX - touchStart.x;
const deltaY = touch.clientY - touchStart.y;
const threshold = 50;
// Determine swipe direction
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// Horizontal swipe
if (deltaX > threshold) {
onSwipeRight?.();
} else if (deltaX < -threshold) {
onSwipeLeft?.();
}
} else {
// Vertical swipe
if (deltaY > threshold) {
onSwipeDown?.();
} else if (deltaY < -threshold) {
onSwipeUp?.();
}
}
setTouchStart(null);
};
return (
<div
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
style={{
width: '100%',
height: '300px',
border: '1px solid',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
Swipe in any direction
</div>
);
};
// Usage
const SwipeDemo = () => (
<SwipeDetector
onSwipeLeft={() => console.log('Swiped left')}
onSwipeRight={() => console.log('Swiped right')}
onSwipeUp={() => console.log('Swiped up')}
onSwipeDown={() => console.log('Swiped down')}
/>
);
// Pinch zoom
const PinchZoom = () => {
const [scale, setScale] = useState(1);
const [initialDistance, setInitialDistance] = useState(null);
const getDistance = (touch1, touch2) => {
const dx = touch1.clientX - touch2.clientX;
const dy = touch1.clientY - touch2.clientY;
return Math.sqrt(dx * dx + dy * dy);
};
const handleTouchStart = (e) => {
if (e.touches.length === 2) {
const distance = getDistance(e.touches[0], e.touches[1]);
setInitialDistance(distance);
}
};
const handleTouchMove = (e) => {
if (e.touches.length === 2 && initialDistance) {
e.preventDefault();
const distance = getDistance(e.touches[0], e.touches[1]);
const newScale = (distance / initialDistance) * scale;
// Clamp scale between 0.5 and 3
setScale(Math.min(Math.max(newScale, 0.5), 3));
}
};
const handleTouchEnd = () => {
setInitialDistance(null);
};
return (
<div
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{
width: '300px',
height: '300px',
border: '1px solid',
overflow: 'hidden',
touchAction: 'none'
}}
>
<div
style={{
transform: \`scale(\${scale})\`,
transformOrigin: 'center',
transition: initialDistance ? 'none' : 'transform 0.3s'
}}
>
Pinch to zoom (scale: {scale.toFixed(2)})
</div>
</div>
);
};
// Draggable with touch support
const TouchDraggable = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
const handleStart = (clientX, clientY) => {
setIsDragging(true);
setStartPos({
x: clientX - position.x,
y: clientY - position.y
});
};
const handleMove = (clientX, clientY) => {
if (isDragging) {
setPosition({
x: clientX - startPos.x,
y: clientY - startPos.y
});
}
};
// Touch handlers
const handleTouchStart = (e) => {
const touch = e.touches[0];
handleStart(touch.clientX, touch.clientY);
};
const handleTouchMove = (e) => {
e.preventDefault();
const touch = e.touches[0];
handleMove(touch.clientX, touch.clientY);
};
// Mouse handlers (for desktop)
const handleMouseDown = (e) => {
handleStart(e.clientX, e.clientY);
};
const handleMouseMove = (e) => {
handleMove(e.clientX, e.clientY);
};
const handleEnd = () => {
setIsDragging(false);
};
useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleEnd);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleEnd);
};
}
}, [isDragging]);
return (
<div
onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleEnd}
style={{
position: 'absolute',
left: position.x,
top: position.y,
width: '100px',
height: '100px',
backgroundColor: 'green',
touchAction: 'none'
}}
>
Drag me
</div>
);
};
Warning: Always set
touchAction: 'none' CSS property when handling touch events to
prevent default browser behaviors. Use e.preventDefault() carefully as it can interfere with
scrolling.
Event Handling Best Practices
- Function references: Use direct references when possible for better performance
- useCallback: Memoize handlers passed to memo'ed children or large lists
- Event delegation: React handles automatically - don't manually manage listeners
- preventDefault/stopPropagation: Use judiciously to control event flow
- Touch events: Support both mouse and touch for best mobile/desktop experience
- Debounce/throttle: Control frequency for expensive operations (search, scroll)