Back to Blog
ReactEventsPatternsTypeScript

React Event Handling Patterns

Master React event handling with proper patterns, performance optimization, and TypeScript.

B
Bootspring Team
Engineering
October 7, 2018
7 min read

Proper event handling in React is crucial for interactive applications. Here's how to do it right.

Basic Event Handling#

1function Button() { 2 // Inline handler 3 return ( 4 <button onClick={() => console.log('clicked')}> 5 Click me 6 </button> 7 ); 8} 9 10// Named handler (better for debugging) 11function Button() { 12 const handleClick = () => { 13 console.log('clicked'); 14 }; 15 16 return <button onClick={handleClick}>Click me</button>; 17} 18 19// Event object 20function Input() { 21 const handleChange = (event) => { 22 console.log('Value:', event.target.value); 23 }; 24 25 return <input onChange={handleChange} />; 26}

Passing Arguments#

1// Using arrow function (creates new function each render) 2function TodoList({ todos, onDelete }) { 3 return ( 4 <ul> 5 {todos.map(todo => ( 6 <li key={todo.id}> 7 {todo.text} 8 <button onClick={() => onDelete(todo.id)}> 9 Delete 10 </button> 11 </li> 12 ))} 13 </ul> 14 ); 15} 16 17// Using data attributes 18function TodoList({ todos, onDelete }) { 19 const handleDelete = (event) => { 20 const id = event.currentTarget.dataset.id; 21 onDelete(id); 22 }; 23 24 return ( 25 <ul> 26 {todos.map(todo => ( 27 <li key={todo.id}> 28 {todo.text} 29 <button data-id={todo.id} onClick={handleDelete}> 30 Delete 31 </button> 32 </li> 33 ))} 34 </ul> 35 ); 36} 37 38// Using curried function 39function TodoList({ todos, onDelete }) { 40 const handleDelete = (id) => () => { 41 onDelete(id); 42 }; 43 44 return ( 45 <ul> 46 {todos.map(todo => ( 47 <li key={todo.id}> 48 {todo.text} 49 <button onClick={handleDelete(todo.id)}>Delete</button> 50 </li> 51 ))} 52 </ul> 53 ); 54}

Event Pooling (Legacy)#

1// React 16 and earlier pooled events 2// This was removed in React 17+ 3 4// Pre-React 17 issue 5function OldComponent() { 6 const handleClick = (event) => { 7 // event properties are nullified after handler 8 setTimeout(() => { 9 console.log(event.target); // null! 10 }, 100); 11 }; 12 13 // Solution was event.persist() 14 const handleClickFixed = (event) => { 15 event.persist(); 16 setTimeout(() => { 17 console.log(event.target); // works 18 }, 100); 19 }; 20} 21 22// React 17+ - no pooling, no persist() needed 23function ModernComponent() { 24 const handleClick = (event) => { 25 setTimeout(() => { 26 console.log(event.target); // works! 27 }, 100); 28 }; 29 30 return <button onClick={handleClick}>Click</button>; 31}

Preventing Default#

1// Prevent form submission 2function Form() { 3 const handleSubmit = (event) => { 4 event.preventDefault(); 5 // Handle form data 6 }; 7 8 return ( 9 <form onSubmit={handleSubmit}> 10 <input name="email" /> 11 <button type="submit">Submit</button> 12 </form> 13 ); 14} 15 16// Prevent link navigation 17function CustomLink({ href, children, onClick }) { 18 const handleClick = (event) => { 19 event.preventDefault(); 20 onClick?.(href); 21 // Custom navigation logic 22 }; 23 24 return <a href={href} onClick={handleClick}>{children}</a>; 25}

Stop Propagation#

1// Stop event bubbling 2function Dropdown({ onClose }) { 3 const handleInnerClick = (event) => { 4 event.stopPropagation(); 5 // Don't trigger parent click handler 6 }; 7 8 return ( 9 <div onClick={onClose}> 10 <div className="dropdown-content" onClick={handleInnerClick}> 11 {/* Click here won't close */} 12 </div> 13 </div> 14 ); 15} 16 17// Modal pattern 18function Modal({ isOpen, onClose, children }) { 19 if (!isOpen) return null; 20 21 return ( 22 <div className="modal-overlay" onClick={onClose}> 23 <div className="modal-content" onClick={e => e.stopPropagation()}> 24 {children} 25 </div> 26 </div> 27 ); 28}

Keyboard Events#

1function SearchInput({ onSearch }) { 2 const handleKeyDown = (event) => { 3 if (event.key === 'Enter') { 4 onSearch(event.target.value); 5 } 6 7 if (event.key === 'Escape') { 8 event.target.blur(); 9 } 10 }; 11 12 return <input onKeyDown={handleKeyDown} placeholder="Search..." />; 13} 14 15// With modifiers 16function ShortcutHandler({ children }) { 17 const handleKeyDown = (event) => { 18 // Cmd/Ctrl + S 19 if ((event.metaKey || event.ctrlKey) && event.key === 's') { 20 event.preventDefault(); 21 handleSave(); 22 } 23 24 // Cmd/Ctrl + Z 25 if ((event.metaKey || event.ctrlKey) && event.key === 'z') { 26 event.preventDefault(); 27 handleUndo(); 28 } 29 }; 30 31 return <div onKeyDown={handleKeyDown}>{children}</div>; 32}

Focus and Blur#

1function Input({ onFocus, onBlur }) { 2 const [focused, setFocused] = useState(false); 3 4 const handleFocus = (event) => { 5 setFocused(true); 6 onFocus?.(event); 7 }; 8 9 const handleBlur = (event) => { 10 setFocused(false); 11 onBlur?.(event); 12 }; 13 14 return ( 15 <input 16 className={focused ? 'focused' : ''} 17 onFocus={handleFocus} 18 onBlur={handleBlur} 19 /> 20 ); 21} 22 23// Focus trap 24function FocusTrap({ children }) { 25 const containerRef = useRef(null); 26 27 const handleKeyDown = (event) => { 28 if (event.key !== 'Tab') return; 29 30 const focusable = containerRef.current.querySelectorAll( 31 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' 32 ); 33 34 const first = focusable[0]; 35 const last = focusable[focusable.length - 1]; 36 37 if (event.shiftKey && document.activeElement === first) { 38 event.preventDefault(); 39 last.focus(); 40 } else if (!event.shiftKey && document.activeElement === last) { 41 event.preventDefault(); 42 first.focus(); 43 } 44 }; 45 46 return ( 47 <div ref={containerRef} onKeyDown={handleKeyDown}> 48 {children} 49 </div> 50 ); 51}

Touch Events#

1function SwipeableCard({ onSwipe }) { 2 const [startX, setStartX] = useState(0); 3 const [offsetX, setOffsetX] = useState(0); 4 5 const handleTouchStart = (event) => { 6 setStartX(event.touches[0].clientX); 7 }; 8 9 const handleTouchMove = (event) => { 10 const currentX = event.touches[0].clientX; 11 setOffsetX(currentX - startX); 12 }; 13 14 const handleTouchEnd = () => { 15 if (Math.abs(offsetX) > 100) { 16 onSwipe(offsetX > 0 ? 'right' : 'left'); 17 } 18 setOffsetX(0); 19 }; 20 21 return ( 22 <div 23 style={{ transform: `translateX(${offsetX}px)` }} 24 onTouchStart={handleTouchStart} 25 onTouchMove={handleTouchMove} 26 onTouchEnd={handleTouchEnd} 27 > 28 Card Content 29 </div> 30 ); 31}

TypeScript Event Types#

1// Common event types 2function TypedEvents() { 3 // Mouse event 4 const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { 5 console.log(event.clientX, event.clientY); 6 }; 7 8 // Change event 9 const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { 10 console.log(event.target.value); 11 }; 12 13 // Form event 14 const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { 15 event.preventDefault(); 16 }; 17 18 // Keyboard event 19 const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { 20 console.log(event.key); 21 }; 22 23 // Focus event 24 const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => { 25 event.target.select(); 26 }; 27 28 return ( 29 <form onSubmit={handleSubmit}> 30 <input 31 onChange={handleChange} 32 onKeyDown={handleKeyDown} 33 onFocus={handleFocus} 34 /> 35 <button onClick={handleClick}>Submit</button> 36 </form> 37 ); 38} 39 40// Generic handler type 41type EventHandler<E extends React.SyntheticEvent> = (event: E) => void; 42 43// Handler with element type 44function Input({ 45 onChange, 46}: { 47 onChange: React.ChangeEventHandler<HTMLInputElement>; 48}) { 49 return <input onChange={onChange} />; 50}

Debounced Events#

1import { useState, useCallback, useRef, useEffect } from 'react'; 2 3// Debounce hook 4function useDebounce(callback, delay) { 5 const timeoutRef = useRef(null); 6 7 const debouncedFn = useCallback((...args) => { 8 clearTimeout(timeoutRef.current); 9 timeoutRef.current = setTimeout(() => { 10 callback(...args); 11 }, delay); 12 }, [callback, delay]); 13 14 useEffect(() => { 15 return () => clearTimeout(timeoutRef.current); 16 }, []); 17 18 return debouncedFn; 19} 20 21// Usage 22function SearchInput({ onSearch }) { 23 const [query, setQuery] = useState(''); 24 25 const debouncedSearch = useDebounce((value) => { 26 onSearch(value); 27 }, 300); 28 29 const handleChange = (event) => { 30 const value = event.target.value; 31 setQuery(value); 32 debouncedSearch(value); 33 }; 34 35 return <input value={query} onChange={handleChange} />; 36}

Event Delegation#

1// Handle events at parent level 2function TodoList({ todos, onToggle, onDelete }) { 3 const handleClick = (event) => { 4 const action = event.target.dataset.action; 5 const id = event.target.closest('[data-id]')?.dataset.id; 6 7 if (!id || !action) return; 8 9 if (action === 'toggle') onToggle(id); 10 if (action === 'delete') onDelete(id); 11 }; 12 13 return ( 14 <ul onClick={handleClick}> 15 {todos.map(todo => ( 16 <li key={todo.id} data-id={todo.id}> 17 <span data-action="toggle">{todo.text}</span> 18 <button data-action="delete">Delete</button> 19 </li> 20 ))} 21 </ul> 22 ); 23}

Custom Event Hooks#

1// useClickOutside 2function useClickOutside(ref, handler) { 3 useEffect(() => { 4 const listener = (event) => { 5 if (!ref.current || ref.current.contains(event.target)) { 6 return; 7 } 8 handler(event); 9 }; 10 11 document.addEventListener('mousedown', listener); 12 document.addEventListener('touchstart', listener); 13 14 return () => { 15 document.removeEventListener('mousedown', listener); 16 document.removeEventListener('touchstart', listener); 17 }; 18 }, [ref, handler]); 19} 20 21// Usage 22function Dropdown() { 23 const [isOpen, setIsOpen] = useState(false); 24 const ref = useRef(null); 25 26 useClickOutside(ref, () => setIsOpen(false)); 27 28 return ( 29 <div ref={ref}> 30 <button onClick={() => setIsOpen(!isOpen)}>Toggle</button> 31 {isOpen && <div className="menu">Content</div>} 32 </div> 33 ); 34}

Best Practices#

Handlers: ✓ Use named functions for readability ✓ Extract complex logic to functions ✓ Keep handlers close to usage ✓ Use useCallback when passing down Performance: ✓ Avoid inline arrow functions in lists ✓ Use event delegation for many items ✓ Debounce frequent events ✓ Memoize callbacks appropriately TypeScript: ✓ Use proper event types ✓ Type event.target correctly ✓ Use generic handlers when needed ✓ Prefer React event types Avoid: ✗ Inline handlers for complex logic ✗ Missing preventDefault when needed ✗ Ignoring cleanup in useEffect ✗ Unnecessary event.stopPropagation

Conclusion#

React event handling is straightforward but has nuances. Use named handlers for clarity, proper TypeScript types for safety, and patterns like debouncing for performance. Understand event bubbling and when to use preventDefault and stopPropagation. For complex scenarios, create custom hooks that encapsulate event logic and cleanup.

Share this article

Help spread the word about Bootspring