JavaScript Event Handling Patterns: A Comprehensive Guide
Event handling forms the backbone of modern JavaScript applications. I’ve implemented these patterns countless times, and each serves a unique purpose in creating responsive web applications.
Event Delegation
Parent elements can handle events for their children, reducing memory usage and improving performance. I particularly value this pattern when working with dynamic content.
const table = document.querySelector('table');
table.addEventListener('click', (event) => {
const cell = event.target.closest('td');
if (!cell) return;
console.log(`Cell clicked: ${cell.textContent}`);
});
Custom Events
Creating custom events enables clean component communication. I use this pattern to maintain loose coupling between different parts of applications.
class ShoppingCart {
addItem(item) {
// Add item logic
const event = new CustomEvent('cartUpdate', {
detail: { item, action: 'add' }
});
document.dispatchEvent(event);
}
}
document.addEventListener('cartUpdate', (e) => {
console.log(`Cart updated: ${e.detail.action} ${e.detail.item}`);
});
Event Pooling
In performance-critical applications, reusing event objects reduces garbage collection overhead.
class EventPool {
constructor(size = 10) {
this.pool = Array(size).fill().map(() => ({ type: null, data: null }));
this.index = 0;
}
acquire(type, data) {
const event = this.pool[this.index];
event.type = type;
event.data = data;
this.index = (this.index + 1) % this.pool.length;
return event;
}
}
Passive Event Listeners
These improve scroll performance, especially on mobile devices.
document.addEventListener('scroll', (e) => {
requestAnimationFrame(() => {
updateScrollPosition(window.scrollY);
});
}, { passive: true });
Event Cleanup
Proper cleanup prevents memory leaks and improves application performance.
class Component {
constructor() {
this.handleResize = this.handleResize.bind(this);
window.addEventListener('resize', this.handleResize);
}
handleResize() {
// Resize logic
}
destroy() {
window.removeEventListener('resize', this.handleResize);
}
}
Event Debouncing
Control event frequency to optimize performance.
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
const handleSearch = debounce((searchTerm) => {
performSearch(searchTerm);
}, 300);
Event Broadcasting
Implement publish-subscribe pattern for application-wide communication.
class EventBus {
constructor() {
this.subscribers = {};
}
subscribe(event, callback) {
if (!this.subscribers[event]) {
this.subscribers[event] = [];
}
this.subscribers[event].push(callback);
return () => this.unsubscribe(event, callback);
}
publish(event, data) {
if (!this.subscribers[event]) return;
this.subscribers[event].forEach(callback => callback(data));
}
unsubscribe(event, callback) {
this.subscribers[event] = this.subscribers[event]
.filter(cb => cb !== callback);
}
}
Event Capturing
Handle events during the capture phase for specific scenarios.
class Modal {
constructor() {
this.modal = document.querySelector('.modal');
this.setupEventCapturing();
}
setupEventCapturing() {
document.addEventListener('click', (e) => {
if (!this.modal.contains(e.target)) {
this.close();
}
}, true);
}
close() {
this.modal.style.display = 'none';
}
}
Practical Implementation Example
Here’s a complete example combining multiple patterns:
class TodoApp {
constructor() {
this.eventBus = new EventBus();
this.todoList = document.querySelector('.todo-list');
this.setupEventHandlers();
}
setupEventHandlers() {
// Event delegation for todo items
this.todoList.addEventListener('click', (e) => {
const todoItem = e.target.closest('.todo-item');
if (!todoItem) return;
if (e.target.matches('.delete-btn')) {
this.deleteTodo(todoItem);
} else if (e.target.matches('.edit-btn')) {
this.editTodo(todoItem);
}
});
// Debounced search
const searchInput = document.querySelector('.search-input');
searchInput.addEventListener('input', debounce((e) => {
this.searchTodos(e.target.value);
}, 300));
// Custom event handling
this.eventBus.subscribe('todoAdded', (todo) => {
this.renderTodo(todo);
});
}
deleteTodo(todoItem) {
todoItem.remove();
this.eventBus.publish('todoDeleted', todoItem.dataset.id);
}
editTodo(todoItem) {
// Edit implementation
}
searchTodos(term) {
// Search implementation
}
renderTodo(todo) {
// Render implementation
}
destroy() {
// Cleanup
this.todoList.removeEventListener('click');
this.eventBus = null;
}
}
These patterns create maintainable, efficient, and scalable applications. The key is choosing the right pattern for specific scenarios while considering performance implications and code maintainability.