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.