Introduction to Events in React
Events are actions or occurrences that happen in the system you are building, which the system tells you about so you can respond to them in some way. In the context of web applications, events are typically user interactions like clicks, keyboard input, mouse movements, form submissions, and more.
React has its own event handling system which is similar to handling events in the DOM, but with some key differences and advantages.
Key Differences from DOM Events
- React events are named using camelCase (e.g.,
onClickinstead ofonclick) - Event handlers are passed as functions rather than strings
- React uses synthetic events for cross-browser compatibility
- You cannot return
falseto prevent default behavior; you must explicitly callpreventDefault()
SyntheticEvent System
React implements a synthetic event system that wraps the browser's native event system. This provides several benefits:
- Cross-browser compatibility: Events behave consistently across all browsers
- Performance optimization: React uses event delegation for better performance
- Automatic cleanup: React handles event listener cleanup to prevent memory leaks
- Event pooling: (Note: this was removed in React 17) Previously, React reused event objects for performance
Real-world analogy: Think of React's synthetic events like a universal translator. Different browsers might "speak different languages" when it comes to events, but React's synthetic event system translates everything into a common language your code can easily understand.
The Event Object
React's synthetic event objects follow the W3C spec, so they have the same properties and methods as native events:
function Button() {
const handleClick = (event) => {
// event is a SyntheticEvent
console.log('Button clicked!');
console.log('Event type:', event.type);
console.log('Target:', event.target);
// Accessing native event
console.log('Native event:', event.nativeEvent);
};
return (
<button onClick={handleClick}>
Click Me
</button>
);
}
Note: React's SyntheticEvent is a cross-browser wrapper around the browser's native event.
If you need access to the underlying browser event, you can use the nativeEvent property.
Common Event Types
Mouse Events
| Event | Description |
|---|---|
onClick |
Fired when an element is clicked |
onDoubleClick |
Fired when an element is double-clicked |
onMouseEnter |
Fired when the mouse enters an element |
onMouseLeave |
Fired when the mouse leaves an element |
onMouseMove |
Fired when the mouse moves within an element |
function MouseEventDemo() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isHovering, setIsHovering] = useState(false);
const handleMouseMove = (e) => {
// Update position relative to the container
setPosition({
x: e.nativeEvent.offsetX,
y: e.nativeEvent.offsetY
});
};
return (
<div
style={{
width: '300px',
height: '200px',
border: '1px solid #ccc',
position: 'relative',
backgroundColor: isHovering ? '#f0f0f0' : 'white'
}}
onMouseMove={handleMouseMove}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<p>Mouse position: {position.x}, {position.y}</p>
{isHovering && (
<div
style={{
position: 'absolute',
left: position.x,
top: position.y,
width: '5px',
height: '5px',
borderRadius: '50%',
backgroundColor: 'red',
transform: 'translate(-50%, -50%)'
}}
/>
)}
</div>
);
}
Keyboard Events
| Event | Description |
|---|---|
onKeyDown |
Fired when a key is pressed down |
onKeyPress |
Fired when a key is pressed (character keys only) |
onKeyUp |
Fired when a key is released |
function KeyboardDemo() {
const [lastKey, setLastKey] = useState('');
const [inputValue, setInputValue] = useState('');
const handleKeyDown = (e) => {
setLastKey(e.key);
// Example: Prevent typing numbers
if (/^\d$/.test(e.key)) {
e.preventDefault();
alert('Numbers are not allowed!');
}
};
return (
<div>
<p>Type something (no numbers allowed):</p>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type here..."
/>
{lastKey && <p>Last key pressed: {lastKey}</p>}
</div>
);
}
Form Events
| Event | Description |
|---|---|
onChange |
Fired when the value of an input element changes |
onSubmit |
Fired when a form is submitted |
onFocus |
Fired when an element receives focus |
onBlur |
Fired when an element loses focus |
We'll cover form events in more detail in the next lecture, but here's a simple example:
function SimpleForm() {
const [formData, setFormData] = useState({
username: '',
email: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prevData => ({
...prevData,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault(); // Prevent page refresh
console.log('Form submitted:', formData);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username:</label>
<input
id="username"
name="username"
value={formData.username}
onChange={handleChange}
onFocus={() => console.log('Username field focused')}
onBlur={() => console.log('Username field blurred')}
/>
</div>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
/>
</div>
<button type="submit">Submit</button>
</form>
);
}
Creating Event Handlers
Inline Event Handlers
The simplest approach is to define event handlers inline:
function InlineHandlerExample() {
return (
<button onClick={() => alert('Button clicked!')}>
Click Me
</button>
);
}
Note: While inline handlers are simple, they can lead to unnecessary re-renders and make your JSX harder to read if the handler logic is complex.
Defined Function Handlers
For more complex handlers, define a function within your component:
function DefinedHandlerExample() {
const handleClick = () => {
// Complex logic here
console.log('Button clicked!');
// More operations...
};
return (
<button onClick={handleClick}>
Click Me
</button>
);
}
Event Handlers with Parameters
When you need to pass additional parameters to an event handler:
function ItemList() {
const items = ['Apple', 'Banana', 'Cherry', 'Date'];
const handleItemClick = (item, index, event) => {
console.log(`Item clicked: ${item} at index ${index}`);
console.log('Event:', event.type);
};
return (
<ul>
{items.map((item, index) => (
<li
key={index}
onClick={(e) => handleItemClick(item, index, e)}
>
{item}
</li>
))}
</ul>
);
}
Event Handler Performance
Avoiding Unnecessary Re-renders
Every time a component re-renders, inline event handlers are recreated, which can cause performance issues in certain scenarios:
❌ Less Efficient
function ChildComponent({ onClick }) {
console.log('Child rendered');
return <button onClick={onClick}>Click</button>;
}
function ParentComponent() {
const [count, setCount] = useState(0);
// This creates a new function on every render
return (
<div>
<p>Count: {count}</p>
<ChildComponent
onClick={() => setCount(count + 1)}
/>
</div>
);
}
The child component will re-render even if just the parent renders.
✅ More Efficient
function ChildComponent({ onClick }) {
console.log('Child rendered');
return <button onClick={onClick}>Click</button>;
}
// Optimize with React.memo
const MemoizedChild = React.memo(ChildComponent);
function ParentComponent() {
const [count, setCount] = useState(0);
// Use useCallback to memoize the function
const handleClick = useCallback(() => {
setCount(prevCount => prevCount + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<MemoizedChild onClick={handleClick} />
</div>
);
}
Now the child component only re-renders when its props actually change.
The useCallback Hook
useCallback helps memoize event handler functions so they don't get recreated on every render:
import React, { useState, useCallback } from 'react';
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
// This function is memoized and only recreated if dependencies change
const handleSearch = useCallback((event) => {
const value = event.target.value;
setSearchTerm(value);
// Fictional search function
fetchResults(value).then(data => {
setResults(data);
});
}, []); // Empty dependency array - function is created once
return (
<div>
<input
type="text"
value={searchTerm}
onChange={handleSearch}
placeholder="Search..."
/>
<ul>
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
Event Propagation in React
Bubbling and Capturing
Like in standard DOM, React events follow the bubbling and capturing phases:
- Bubbling: Events "bubble up" from the target element to its ancestors
- Capturing: Events are first captured from the root down to the target
By default, React event handlers listen for the bubbling phase. To listen for events during the capturing phase, append "Capture" to the event name:
function PropagationDemo() {
return (
<div
onClick={() => console.log('Outer div clicked (bubble)')}
onClickCapture={() => console.log('Outer div clicked (capture)')}
style={{ padding: '20px', backgroundColor: '#f0f0f0' }}
>
<div
onClick={() => console.log('Inner div clicked (bubble)')}
onClickCapture={() => console.log('Inner div clicked (capture)')}
style={{ padding: '20px', backgroundColor: '#d0d0d0' }}
>
<button
onClick={() => console.log('Button clicked (bubble)')}
onClickCapture={() => console.log('Button clicked (capture)')}
>
Click me
</button>
</div>
</div>
);
}
// Output when button is clicked:
// Outer div clicked (capture)
// Inner div clicked (capture)
// Button clicked (capture)
// Button clicked (bubble)
// Inner div clicked (bubble)
// Outer div clicked (bubble)
Stopping Propagation
Sometimes you want to prevent events from propagating to parent elements:
function StopPropagationDemo() {
return (
<div
onClick={() => console.log('Outer div clicked')}
style={{ padding: '20px', backgroundColor: '#f0f0f0' }}
>
<div
onClick={() => console.log('Inner div clicked')}
style={{ padding: '20px', backgroundColor: '#d0d0d0' }}
>
<button
onClick={(e) => {
e.stopPropagation(); // Stop the event from bubbling up
console.log('Button clicked');
}}
>
Click me (stops propagation)
</button>
</div>
</div>
);
}
// Output when button is clicked:
// Button clicked
Preventing Default Behavior
To prevent a browser's default action (like form submission or link navigation):
function PreventDefaultDemo() {
return (
<div>
<h3>Form with preventDefault</h3>
<form
onSubmit={(e) => {
e.preventDefault(); // Prevents page refresh
console.log('Form submitted without page refresh');
}}
>
<input type="text" placeholder="Enter something" />
<button type="submit">Submit Form</button>
</form>
<h3>Link with preventDefault</h3>
<a
href="https://example.com"
onClick={(e) => {
e.preventDefault(); // Prevents navigation
console.log('Link clicked but no navigation occurred');
}}
>
Click me (won't navigate)
</a>
</div>
);
}
Real-world example: Think of event propagation like how sound travels in a room. If someone speaks (the event), people nearby hear it first, and then people further away (bubbling). Stopping propagation is like soundproofing a room so sound doesn't travel outside of it.
Advanced Event Handling Patterns
Event Delegation
Event delegation is a pattern where you attach a single event listener to a parent element instead of individual listeners on many child elements. React's event system uses this internally for performance, but you can also leverage it in your code:
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: false },
{ id: 2, text: 'Build an app', completed: false },
{ id: 3, text: 'Deploy to production', completed: false }
]);
// Single event handler for the entire list
const handleItemClick = (e) => {
// Find the closest li element
const li = e.target.closest('li');
if (!li) return;
// Get the todo id from the data attribute
const todoId = Number(li.dataset.id);
// Toggle the completed status
setTodos(todos.map(todo =>
todo.id === todoId
? { ...todo, completed: !todo.completed }
: todo
));
};
return (
<ul onClick={handleItemClick}>
{todos.map(todo => (
<li
key={todo.id}
data-id={todo.id}
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
cursor: 'pointer'
}}
>
{todo.text}
</li>
))}
</ul>
);
}
Custom Event Handling Hooks
Create reusable custom hooks for common event handling patterns:
// Custom hook for handling hover state
function useHover() {
const [isHovering, setIsHovering] = useState(false);
const handleMouseEnter = () => setIsHovering(true);
const handleMouseLeave = () => setIsHovering(false);
// Return both the state and the event handlers
return {
isHovering,
hoverProps: {
onMouseEnter: handleMouseEnter,
onMouseLeave: handleMouseLeave
}
};
}
// Custom hook for handling drag and drop
function useDrag() {
const [isDragging, setIsDragging] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleDragStart = (e) => {
setIsDragging(true);
// Store initial position
const startX = e.clientX - position.x;
const startY = e.clientY - position.y;
const handleDragMove = (moveEvent) => {
setPosition({
x: moveEvent.clientX - startX,
y: moveEvent.clientY - startY
});
};
const handleDragEnd = () => {
setIsDragging(false);
window.removeEventListener('mousemove', handleDragMove);
window.removeEventListener('mouseup', handleDragEnd);
};
window.addEventListener('mousemove', handleDragMove);
window.addEventListener('mouseup', handleDragEnd);
};
return {
isDragging,
position,
dragProps: {
onMouseDown: handleDragStart,
style: {
position: 'absolute',
left: `${position.x}px`,
top: `${position.y}px`,
cursor: isDragging ? 'grabbing' : 'grab'
}
}
};
}
// Usage example
function DraggableElement() {
const { isDragging, dragProps } = useDrag();
const { isHovering, hoverProps } = useHover();
return (
<div
{...dragProps}
{...hoverProps}
style={{
...dragProps.style,
backgroundColor: isHovering ? 'lightblue' : 'lightgray',
padding: '20px',
width: '100px',
height: '100px',
borderRadius: '8px',
boxShadow: isDragging
? '0px 10px 20px rgba(0,0,0,0.2)'
: '0px 2px 5px rgba(0,0,0,0.1)'
}}
>
{isDragging ? 'Dragging' : 'Drag me'}
</div>
);
}
Real-World Applications
Interactive Data Table
function DataTable() {
const [data, setData] = useState([
{ id: 1, name: 'Alice', age: 29, role: 'Developer' },
{ id: 2, name: 'Bob', age: 35, role: 'Designer' },
{ id: 3, name: 'Charlie', age: 42, role: 'Manager' },
{ id: 4, name: 'Diana', age: 25, role: 'Tester' }
]);
const [sortField, setSortField] = useState(null);
const [sortDirection, setSortDirection] = useState('asc');
const [selectedRow, setSelectedRow] = useState(null);
const handleHeaderClick = (field) => {
// If clicking the same field, toggle direction, otherwise sort ascending
if (sortField === field) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortDirection('asc');
}
};
const handleRowClick = (id) => {
setSelectedRow(selectedRow === id ? null : id);
};
// Sort the data
const sortedData = [...data].sort((a, b) => {
if (!sortField) return 0;
const comparison = a[sortField] > b[sortField] ? 1 : -1;
return sortDirection === 'asc' ? comparison : -comparison;
});
return (
<table>
<thead>
<tr>
<th
onClick={() => handleHeaderClick('name')}
style={{ cursor: 'pointer' }}
>
Name
{sortField === 'name' && (
sortDirection === 'asc' ? '▲' : '▼'
)}
</th>
<th
onClick={() => handleHeaderClick('age')}
style={{ cursor: 'pointer' }}
>
Age
{sortField === 'age' && (
sortDirection === 'asc' ? '▲' : '▼'
)}
</th>
<th
onClick={() => handleHeaderClick('role')}
style={{ cursor: 'pointer' }}
>
Role
{sortField === 'role' && (
sortDirection === 'asc' ? '▲' : '▼'
)}
</th>
</tr>
</thead>
<tbody>
{sortedData.map(row => (
<tr
key={row.id}
onClick={() => handleRowClick(row.id)}
style={{
backgroundColor: selectedRow === row.id ? '#e0f7fa' : 'transparent',
cursor: 'pointer'
}}
>
<td>{row.name}</td>
<td>{row.age}</td>
<td>{row.role}</td>
</tr>
))}
</tbody>
</table>
);
}
Interactive Image Gallery
function ImageGallery() {
const images = [
{ id: 1, src: '/images/image1.jpg', alt: 'Mountain landscape' },
{ id: 2, src: '/images/image2.jpg', alt: 'Beach sunset' },
{ id: 3, src: '/images/image3.jpg', alt: 'Forest path' },
{ id: 4, src: '/images/image4.jpg', alt: 'City skyline' }
];
const [selectedImage, setSelectedImage] = useState(null);
const [lightboxOpen, setLightboxOpen] = useState(false);
const handleImageClick = (image) => {
setSelectedImage(image);
setLightboxOpen(true);
};
const handleLightboxClose = () => {
setLightboxOpen(false);
};
const handlePrevImage = (e) => {
e.stopPropagation(); // Prevent closing the lightbox
const currentIndex = images.findIndex(img => img.id === selectedImage.id);
const prevIndex = (currentIndex - 1 + images.length) % images.length;
setSelectedImage(images[prevIndex]);
};
const handleNextImage = (e) => {
e.stopPropagation(); // Prevent closing the lightbox
const currentIndex = images.findIndex(img => img.id === selectedImage.id);
const nextIndex = (currentIndex + 1) % images.length;
setSelectedImage(images[nextIndex]);
};
// Handle keyboard navigation
useEffect(() => {
const handleKeydown = (e) => {
if (!lightboxOpen) return;
switch (e.key) {
case 'ArrowLeft':
handlePrevImage(e);
break;
case 'ArrowRight':
handleNextImage(e);
break;
case 'Escape':
handleLightboxClose();
break;
default:
break;
}
};
window.addEventListener('keydown', handleKeydown);
return () => window.removeEventListener('keydown', handleKeydown);
}, [lightboxOpen, selectedImage]);
return (
<div>
<div className="gallery-grid">
{images.map(image => (
<div
key={image.id}
className="gallery-item"
onClick={() => handleImageClick(image)}
>
<img src={image.src} alt={image.alt} />
</div>
))}
</div>
{lightboxOpen && selectedImage && (
<div
className="lightbox-overlay"
onClick={handleLightboxClose}
>
<div className="lightbox-content">
<img src={selectedImage.src} alt={selectedImage.alt} />
<button
className="lightbox-prev"
onClick={handlePrevImage}
>
←
</button>
<button
className="lightbox-next"
onClick={handleNextImage}
>
→
</button>
<button
className="lightbox-close"
onClick={handleLightboxClose}
>
×
</button>
</div>
</div>
)}
</div>
);
}
Handling Events in Class Components
Although functional components are now the recommended approach, it's useful to understand how event handling works in class components:
class ButtonCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
// Binding is necessary to make 'this' work in the callback
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(prevState => ({
count: prevState.count + 1
}));
}
// Alternative method using arrow function (no binding needed)
handleReset = () => {
this.setState({ count: 0 });
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.handleClick}>
Increment
</button>
<button onClick={this.handleReset}>
Reset
</button>
</div>
);
}
}
Key differences from functional components:
- Need to bind methods in the constructor (or use arrow functions)
- Access state and props through
this - No hooks like
useCallbackfor optimization
Practice Exercises
Exercise 1: Click Counter
Create a component with multiple buttons that:
- Increments a counter when the first button is clicked
- Decrements the counter when the second button is clicked
- Resets the counter when the third button is clicked
- Prevents the counter from going below zero
Exercise 2: Color Picker
Build a color picker component that:
- Displays a set of color swatches
- Highlights the currently selected color
- Changes the background color of a preview area when a swatch is clicked
- Allows typing a hex color value in an input field
Exercise 3: Drag and Drop List
Create a sortable list component that:
- Displays a list of items
- Allows items to be dragged and reordered
- Provides visual feedback during dragging
- Updates the list order when an item is dropped
Summary
In this lecture, we've covered:
- React's synthetic event system and its benefits
- Common event types and their usage (mouse, keyboard, form)
- Creating and optimizing event handlers
- Event propagation, bubbling, and capturing
- Preventing default behavior and stopping propagation
- Advanced patterns like event delegation and custom hooks
- Real-world examples of interactive components
- Event handling in class components vs. functional components
Understanding React's event handling system is crucial for building interactive and responsive applications. It allows you to create seamless user experiences while leveraging React's performance optimizations.