Event Handling
Learn how to handle user interactions in React through event handlers and create responsive, interactive components.
Learning Objectives
- Understand React's event system and synthetic events
- Handle common events like clicks, changes, and form submissions
- Learn event handler patterns and best practices
- Master controlled components and form handling
- Handle keyboard events and accessibility
- Prevent default behaviors and event propagation
Event Handling
Event handling is how React components respond to user interactions. React uses a unified event system called SyntheticEvents that works consistently across all browsers and provides a clean API for handling user input.
React's Event System
React wraps native DOM events in SyntheticEvent objects that:
- Work consistently across all browsers
- Have the same interface as native events
- Provide additional features like event pooling
- Include preventDefault() and stopPropagation()
Basic Event Handler Syntax
function Button() { // Event handler function const handleClick = (event) => { console.log('Button clicked!'); console.log('Event object:', event); }; return ( <button onClick={handleClick}> Click me! </button> ); } // Inline event handler (for simple actions) function SimpleButton() { return ( <button onClick={() => console.log('Clicked!')}> Click me! </button> ); }
Common Event Types
1. Click Events
function ClickExample() { const [count, setCount] = useState(0); const [message, setMessage] = useState(''); const handleClick = () => { setCount(count + 1); setMessage(`Button clicked ${count + 1} times!`); }; const handleDoubleClick = () => { setMessage('Double clicked!'); setCount(count + 2); }; const handleRightClick = (event) => { event.preventDefault(); // Prevent context menu setMessage('Right clicked!'); }; return ( <div> <h3>Click Events Demo</h3> <p>{message}</p> <p>Count: {count}</p> <button onClick={handleClick}> Single Click (+1) </button> <button onDoubleClick={handleDoubleClick}> Double Click (+2) </button> <button onContextMenu={handleRightClick}> Right Click (no menu) </button> </div> ); }
2. Form Events - Input Changes
function InputExample() { const [text, setText] = useState(''); const [email, setEmail] = useState(''); const [number, setNumber] = useState(0); const handleTextChange = (event) => { setText(event.target.value); }; const handleEmailChange = (event) => { setEmail(event.target.value); }; const handleNumberChange = (event) => { setNumber(parseInt(event.target.value) || 0); }; return ( <div> <h3>Input Events Demo</h3> <div className="form-group"> <label>Text Input:</label> <input type="text" value={text} onChange={handleTextChange} placeholder="Type something..." /> <p>You typed: {text}</p> <p>Character count: {text.length}</p> </div> <div className="form-group"> <label>Email Input:</label> <input type="email" value={email} onChange={handleEmailChange} placeholder="Enter email..." /> <p>Email: {email}</p> <p>Valid email: {email.includes('@') ? 'Yes' : 'No'}</p> </div> <div className="form-group"> <label>Number Input:</label> <input type="number" value={number} onChange={handleNumberChange} /> <p>Number: {number}</p> <p>Double: {number * 2}</p> </div> </div> ); }
3. Select and Checkbox Events
function FormControls() { const [selectedOption, setSelectedOption] = useState(''); const [isChecked, setIsChecked] = useState(false); const [selectedCheckboxes, setSelectedCheckboxes] = useState([]); const [selectedRadio, setSelectedRadio] = useState(''); const handleSelectChange = (event) => { setSelectedOption(event.target.value); }; const handleCheckboxChange = (event) => { setIsChecked(event.target.checked); }; const handleMultiCheckboxChange = (value) => { setSelectedCheckboxes(prev => { if (prev.includes(value)) { return prev.filter(item => item !== value); } else { return [...prev, value]; } }); }; const handleRadioChange = (event) => { setSelectedRadio(event.target.value); }; return ( <div> <h3>Form Controls Demo</h3> {/* Select Dropdown */} <div className="form-group"> <label>Choose a color:</label> <select value={selectedOption} onChange={handleSelectChange}> <option value="">-- Select a color --</option> <option value="red">Red</option> <option value="blue">Blue</option> <option value="green">Green</option> <option value="yellow">Yellow</option> </select> <p>Selected: {selectedOption}</p> </div> {/* Single Checkbox */} <div className="form-group"> <label> <input type="checkbox" checked={isChecked} onChange={handleCheckboxChange} /> Subscribe to newsletter </label> <p>Subscribed: {isChecked ? 'Yes' : 'No'}</p> </div> {/* Multiple Checkboxes */} <div className="form-group"> <label>Select your interests:</label> {['React', 'JavaScript', 'CSS', 'Node.js'].map(interest => ( <label key={interest}> <input type="checkbox" checked={selectedCheckboxes.includes(interest)} onChange={() => handleMultiCheckboxChange(interest)} /> {interest} </label> ))} <p>Selected: {selectedCheckboxes.join(', ')}</p> </div> {/* Radio Buttons */} <div className="form-group"> <label>Choose your experience level:</label> {['Beginner', 'Intermediate', 'Advanced'].map(level => ( <label key={level}> <input type="radio" name="experience" value={level} checked={selectedRadio === level} onChange={handleRadioChange} /> {level} </label> ))} <p>Experience: {selectedRadio}</p> </div> </div> ); }
4. Form Submission
function ContactForm() { const [formData, setFormData] = useState({ name: '', email: '', message: '', newsletter: false }); const [errors, setErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const [submitMessage, setSubmitMessage] = useState(''); const handleChange = (event) => { const { name, value, type, checked } = event.target; setFormData(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value })); // Clear error when user starts typing if (errors[name]) { setErrors(prev => ({ ...prev, [name]: '' })); } }; const validateForm = () => { const newErrors = {}; if (!formData.name.trim()) { newErrors.name = 'Name is required'; } if (!formData.email.trim()) { newErrors.email = 'Email is required'; } else if (!/S+@S+.S+/.test(formData.email)) { newErrors.email = 'Email is invalid'; } if (!formData.message.trim()) { newErrors.message = 'Message is required'; } else if (formData.message.length < 10) { newErrors.message = 'Message must be at least 10 characters'; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = async (event) => { event.preventDefault(); // Prevent page reload if (!validateForm()) { return; } setIsSubmitting(true); setSubmitMessage(''); try { // Simulate API call await new Promise(resolve => setTimeout(resolve, 2000)); setSubmitMessage('Message sent successfully!'); setFormData({ name: '', email: '', message: '', newsletter: false }); } catch (error) { setSubmitMessage('Error sending message. Please try again.'); } finally { setIsSubmitting(false); } }; return ( <div> <h3>Contact Form</h3> <form onSubmit={handleSubmit}> <div className="form-group"> <label>Name:</label> <input type="text" name="name" value={formData.name} onChange={handleChange} className={errors.name ? 'error' : ''} /> {errors.name && <span className="error-message">{errors.name}</span>} </div> <div className="form-group"> <label>Email:</label> <input type="email" name="email" value={formData.email} onChange={handleChange} className={errors.email ? 'error' : ''} /> {errors.email && <span className="error-message">{errors.email}</span>} </div> <div className="form-group"> <label>Message:</label> <textarea name="message" value={formData.message} onChange={handleChange} rows="4" className={errors.message ? 'error' : ''} /> {errors.message && <span className="error-message">{errors.message}</span>} </div> <div className="form-group"> <label> <input type="checkbox" name="newsletter" checked={formData.newsletter} onChange={handleChange} /> Subscribe to newsletter </label> </div> <button type="submit" disabled={isSubmitting} className="submit-button" > {isSubmitting ? 'Sending...' : 'Send Message'} </button> {submitMessage && ( <p className={submitMessage.includes('Error') ? 'error-message' : 'success-message'}> {submitMessage} </p> )} </form> {/* Debug info */} <div className="debug-info"> <h4>Form Data:</h4> <pre>{JSON.stringify(formData, null, 2)}</pre> </div> </div> ); }
5. Keyboard Events
function KeyboardExample() { const [pressedKey, setPressedKey] = useState(''); const [inputValue, setInputValue] = useState(''); const [keyHistory, setKeyHistory] = useState([]); const handleKeyDown = (event) => { setPressedKey(`Key Down: ${event.key}`); // Handle special keys if (event.key === 'Enter') { console.log('Enter pressed!'); } if (event.key === 'Escape') { setInputValue(''); console.log('Input cleared!'); } // Handle key combinations if (event.ctrlKey && event.key === 's') { event.preventDefault(); console.log('Save shortcut pressed!'); } }; const handleKeyUp = (event) => { setPressedKey(`Key Up: ${event.key}`); }; const handleKeyPress = (event) => { // Note: keypress is deprecated, use keydown instead setKeyHistory(prev => [...prev.slice(-9), event.key]); }; const handleInputKeyDown = (event) => { if (event.key === 'Enter') { alert(`You typed: ${inputValue}`); } }; return ( <div> <h3>Keyboard Events Demo</h3> <div className="keyboard-area"> <p>Click here and press any key:</p> <div tabIndex="0" // Makes div focusable onKeyDown={handleKeyDown} onKeyUp={handleKeyUp} style={{ border: '2px solid #ccc', padding: '20px', backgroundColor: '#f9f9f9', outline: 'none' }} > Focus this area and press keys </div> <p>{pressedKey}</p> </div> <div className="input-area"> <label>Type and press Enter:</label> <input type="text" value={inputValue} onChange={(e) => setInputValue(e.target.value)} onKeyDown={handleInputKeyDown} placeholder="Press Enter to alert" /> </div> <div className="key-history"> <h4>Last 10 keys pressed:</h4> <p>{keyHistory.join(' → ')}</p> </div> <div className="shortcuts"> <h4>Try these shortcuts:</h4> <ul> <li>Ctrl+S: Save (prevented)</li> <li>Escape: Clear input</li> <li>Enter: Submit input</li> </ul> </div> </div> ); }
6. Mouse Events
function MouseExample() { const [mouseInfo, setMouseInfo] = useState({ x: 0, y: 0, isOver: false, isDown: false }); const [clickCount, setClickCount] = useState(0); const handleMouseMove = (event) => { setMouseInfo(prev => ({ ...prev, x: event.clientX, y: event.clientY })); }; const handleMouseEnter = () => { setMouseInfo(prev => ({ ...prev, isOver: true })); }; const handleMouseLeave = () => { setMouseInfo(prev => ({ ...prev, isOver: false, isDown: false })); }; const handleMouseDown = () => { setMouseInfo(prev => ({ ...prev, isDown: true })); }; const handleMouseUp = () => { setMouseInfo(prev => ({ ...prev, isDown: false })); setClickCount(prev => prev + 1); }; const handleDragStart = (event) => { event.dataTransfer.setData('text/plain', 'Dragged item'); }; const handleDrop = (event) => { event.preventDefault(); const data = event.dataTransfer.getData('text/plain'); console.log('Dropped:', data); }; const handleDragOver = (event) => { event.preventDefault(); // Allow drop }; return ( <div> <h3>Mouse Events Demo</h3> <div className="mouse-area" onMouseMove={handleMouseMove} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} onMouseDown={handleMouseDown} onMouseUp={handleMouseUp} style={{ width: '300px', height: '200px', border: '2px solid #333', backgroundColor: mouseInfo.isOver ? '#e6f3ff' : '#f9f9f9', cursor: mouseInfo.isDown ? 'grabbing' : 'grab', display: 'flex', alignItems: 'center', justifyContent: 'center', userSelect: 'none' }} > <div> <p>Mouse Position: ({mouseInfo.x}, {mouseInfo.y})</p> <p>Mouse Over: {mouseInfo.isOver ? 'Yes' : 'No'}</p> <p>Mouse Down: {mouseInfo.isDown ? 'Yes' : 'No'}</p> <p>Click Count: {clickCount}</p> </div> </div> <div className="drag-drop-demo"> <h4>Drag and Drop:</h4> <div draggable onDragStart={handleDragStart} style={{ padding: '10px', backgroundColor: '#4CAF50', color: 'white', display: 'inline-block', cursor: 'grab', margin: '10px' }} > Drag me! </div> <div onDrop={handleDrop} onDragOver={handleDragOver} style={{ width: '200px', height: '100px', border: '2px dashed #ccc', display: 'flex', alignItems: 'center', justifyContent: 'center', margin: '10px' }} > Drop zone </div> </div> </div> ); }
Event Handler Patterns
1. Event Handler with Parameters
function ButtonList() { const [selectedId, setSelectedId] = useState(null); const items = [ { id: 1, name: 'Item 1', color: 'red' }, { id: 2, name: 'Item 2', color: 'blue' }, { id: 3, name: 'Item 3', color: 'green' } ]; // Method 1: Arrow function in JSX const handleClick1 = (id, name) => { setSelectedId(id); console.log(`Clicked ${name}`); }; // Method 2: Curried function const handleClick2 = (id, name) => (event) => { setSelectedId(id); console.log(`Clicked ${name}`, event); }; // Method 3: Data attributes const handleClick3 = (event) => { const id = parseInt(event.target.dataset.id); const name = event.target.dataset.name; setSelectedId(id); console.log(`Clicked ${name}`); }; return ( <div> <h3>Event Handlers with Parameters</h3> <p>Selected ID: {selectedId}</p> <div> <h4>Method 1: Arrow function in JSX</h4> {items.map(item => ( <button key={item.id} onClick={() => handleClick1(item.id, item.name)} style={{ backgroundColor: item.color, margin: '5px' }} > {item.name} </button> ))} </div> <div> <h4>Method 2: Curried function</h4> {items.map(item => ( <button key={item.id} onClick={handleClick2(item.id, item.name)} style={{ backgroundColor: item.color, margin: '5px' }} > {item.name} </button> ))} </div> <div> <h4>Method 3: Data attributes</h4> {items.map(item => ( <button key={item.id} data-id={item.id} data-name={item.name} onClick={handleClick3} style={{ backgroundColor: item.color, margin: '5px' }} > {item.name} </button> ))} </div> </div> ); }
2. Event Delegation
function EventDelegation() { const [clickedItem, setClickedItem] = useState(''); const handleContainerClick = (event) => { // Check if clicked element is a button if (event.target.tagName === 'BUTTON') { const action = event.target.dataset.action; const value = event.target.textContent; setClickedItem(`Action: ${action}, Value: ${value}`); } }; return ( <div> <h3>Event Delegation Example</h3> <p>Clicked: {clickedItem}</p> {/* Single event handler for multiple buttons */} <div onClick={handleContainerClick}> <button data-action="save">Save</button> <button data-action="delete">Delete</button> <button data-action="edit">Edit</button> <button data-action="cancel">Cancel</button> </div> </div> ); }
3. Debounced Events
function SearchInput() { const [query, setQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); // Debounce search to avoid too many API calls useEffect(() => { if (!query.trim()) { setSearchResults([]); return; } setIsSearching(true); const timeoutId = setTimeout(async () => { try { // Simulate API call await new Promise(resolve => setTimeout(resolve, 500)); // Mock search results const results = [ `Result for "${query}" #1`, `Result for "${query}" #2`, `Result for "${query}" #3` ]; setSearchResults(results); } catch (error) { console.error('Search failed:', error); } finally { setIsSearching(false); } }, 300); // 300ms delay return () => clearTimeout(timeoutId); }, [query]); return ( <div> <h3>Debounced Search</h3> <input type="text" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search..." /> {isSearching && <p>Searching...</p>} <ul> {searchResults.map((result, index) => ( <li key={index}>{result}</li> ))} </ul> </div> ); }
Best Practices
1. Prevent Default and Stop Propagation
function LinkExample() { const handleLinkClick = (event) => { event.preventDefault(); // Don't navigate console.log('Link clicked but navigation prevented'); }; const handleContainerClick = () => { console.log('Container clicked'); }; const handleButtonClick = (event) => { event.stopPropagation(); // Don't bubble to container console.log('Button clicked'); }; return ( <div onClick={handleContainerClick} style={{ padding: '20px', border: '1px solid #ccc' }}> <p>Container (click anywhere)</p> <a href="https://example.com" onClick={handleLinkClick}> Prevented Link </a> <button onClick={handleButtonClick}> Button (stops propagation) </button> </div> ); }
2. Accessible Event Handling
function AccessibleButton() { const [isPressed, setIsPressed] = useState(false); const handleActivation = () => { setIsPressed(!isPressed); console.log('Button activated'); }; const handleKeyDown = (event) => { // Handle Enter and Space keys for accessibility if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); handleActivation(); } }; return ( <div role="button" tabIndex="0" aria-pressed={isPressed} onClick={handleActivation} onKeyDown={handleKeyDown} style={{ padding: '10px 20px', backgroundColor: isPressed ? '#007bff' : '#f8f9fa', color: isPressed ? 'white' : 'black', border: '1px solid #ccc', cursor: 'pointer', outline: 'none' }} > {isPressed ? 'Pressed' : 'Not Pressed'} </div> ); }
3. Performance Considerations
function PerformantList() { const [items, setItems] = useState( Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}` })) ); // ❌ Bad - creates new function on every render const badExample = () => { return items.map(item => ( <div key={item.id} onClick={() => console.log(item.id)}> {item.name} </div> )); }; // ✅ Good - stable function reference const handleItemClick = useCallback((event) => { const id = parseInt(event.target.dataset.id); console.log('Clicked item:', id); }, []); return ( <div onClick={handleItemClick}> {items.slice(0, 10).map(item => ( <div key={item.id} data-id={item.id}> {item.name} </div> ))} </div> ); }
Common Event Handling Mistakes
1. Calling Function Instead of Passing Reference
// ❌ Wrong - calls function immediately <button onClick={handleClick()}>Click me</button> // ✅ Correct - passes function reference <button onClick={handleClick}>Click me</button> // ✅ Correct - arrow function for parameters <button onClick={() => handleClick(id)}>Click me</button>
2. Forgetting to Bind Context
// ❌ Problematic with class components class MyComponent extends Component { handleClick() { console.log(this); // undefined in strict mode } render() { return <button onClick={this.handleClick}>Click</button>; } } // ✅ Solutions for class components class MyComponent extends Component { // Method 1: Arrow function handleClick = () => { console.log(this); // correctly bound } // Method 2: Bind in constructor constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } // Method 3: Arrow function in JSX render() { return <button onClick={() => this.handleClick()}>Click</button>; } }
Summary
Event handling is essential for creating interactive React components:
✅ Use consistent event handler naming (handleEventName)
✅ Prevent default behavior when needed with preventDefault()
✅ Stop event bubbling with stopPropagation() when appropriate
✅ Handle keyboard events for accessibility
✅ Use controlled components for form inputs
✅ Consider performance with large lists and frequent events
What's Next?
In the next lesson, we'll explore Conditional Rendering - how to show or hide content based on state and props. You'll learn:
- Different conditional rendering techniques
- Ternary operators vs logical AND
- Switch statements for multiple conditions
- Guard clauses and early returns
- Complex conditional patterns
Conditional rendering lets you create dynamic UIs that respond to user interactions and application state!