Learn how to handle user interactions in React through event handlers and create responsive, interactive components.
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 wraps native DOM events in SyntheticEvent objects that:
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
// ❌ 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>
// ❌ 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>;
}
}
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
In the next lesson, we'll explore Conditional Rendering - how to show or hide content based on state and props. You'll learn:
Conditional rendering lets you create dynamic UIs that respond to user interactions and application state!