Back to Tutorial
State and Events
Hands-on Practice
25 min

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!