JavaScript Closures Tutorial - Section 3: Common Use Cases
Event handlers are one of the most common places where closures shine. They allow handlers to access variables from their creation context.
function createButton(label, onClick) {
const button = document.createElement('button');
button.textContent = label;
// Closure: handler has access to label
button.addEventListener('click', function() {
console.log(`${label} button clicked`);
onClick();
});
return button;
}
const saveButton = createButton('Save', () => {
console.log('Saving data...');
});
const cancelButton = createButton('Cancel', () => {
console.log('Cancelling...');
});
document.body.append(saveButton, cancelButton);
Use closures to maintain state across multiple event invocations:
function createCounter() {
let count = 0;
const button = document.createElement('button');
button.textContent = 'Click me!';
button.addEventListener('click', function() {
count++;
button.textContent = `Clicked ${count} times`;
});
return button;
}
const counter1 = createCounter();
const counter2 = createCounter();
document.body.append(counter1, counter2);
// Each button maintains its own count!
Create reusable event handler factories:
function createClickHandler(config) {
let clickCount = 0;
const { maxClicks = Infinity, onMaxReached, onEachClick } = config;
return function(event) {
clickCount++;
if (onEachClick) {
onEachClick(clickCount, event);
}
if (clickCount >= maxClicks) {
event.target.disabled = true;
if (onMaxReached) {
onMaxReached(clickCount);
}
}
};
}
// Create a button that can only be clicked 3 times
const limitedButton = document.createElement('button');
limitedButton.textContent = 'Limited Clicks';
limitedButton.addEventListener('click', createClickHandler({
maxClicks: 3,
onEachClick: (count) => {
console.log(`Click ${count}`);
},
onMaxReached: () => {
console.log('Max clicks reached!');
limitedButton.textContent = 'Disabled';
}
}));
document.body.appendChild(limitedButton);
function createValidator(rules) {
const errors = {};
return function(fieldName, value) {
const fieldRules = rules[fieldName];
if (!fieldRules) return true;
errors[fieldName] = [];
for (const rule of fieldRules) {
if (!rule.test(value)) {
errors[fieldName].push(rule.message);
}
}
return errors[fieldName].length === 0;
};
}
const validate = createValidator({
username: [
{
test: v => v.length >= 3,
message: 'Username must be at least 3 characters'
},
{
test: v => /^[a-zA-Z0-9_]+$/.test(v),
message: 'Username can only contain letters, numbers, and underscores'
}
],
email: [
{
test: v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
message: 'Invalid email format'
}
]
});
// Use in event handlers
document.getElementById('username').addEventListener('blur', function(e) {
const isValid = validate('username', e.target.value);
if (!isValid) {
console.log('Validation errors:', validate.errors);
}
});
Use closures to implement debouncing for search inputs:
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Search handler
function searchAPI(query) {
console.log('Searching for:', query);
// Make API call...
}
const debouncedSearch = debounce(searchAPI, 300);
document.getElementById('search').addEventListener('input', function(e) {
debouncedSearch(e.target.value);
// Only calls searchAPI after user stops typing for 300ms
});
Implement throttling for scroll events:
function throttle(fn, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
function handleScroll() {
console.log('Scroll position:', window.scrollY);
// Update UI based on scroll...
}
const throttledScroll = throttle(handleScroll, 100);
window.addEventListener('scroll', throttledScroll);
// Only calls handleScroll once every 100ms
function createDelegatedHandler(selector, handler) {
return function(event) {
const target = event.target.closest(selector);
if (target) {
handler.call(target, event);
}
};
}
// Handle clicks on any button inside the container
document.getElementById('container').addEventListener('click',
createDelegatedHandler('button', function(event) {
console.log('Button clicked:', this.textContent);
// 'this' refers to the matched button
})
);
Create an event handler that only fires once:
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
const button = document.getElementById('submit');
button.addEventListener('click', once(function() {
console.log('Form submitted!');
// This will only log once, no matter how many times clicked
}));
function createEventEmitter() {
const events = {};
return {
on(event, handler) {
if (!events[event]) {
events[event] = [];
}
events[event].push(handler);
// Return unsubscribe function
return () => {
const index = events[event].indexOf(handler);
if (index > -1) {
events[event].splice(index, 1);
}
};
},
emit(event, data) {
if (!events[event]) return;
events[event].forEach(handler => handler(data));
},
once(event, handler) {
const unsubscribe = this.on(event, (data) => {
handler(data);
unsubscribe();
});
return unsubscribe;
}
};
}
const emitter = createEventEmitter();
// Subscribe to events
const unsubscribe = emitter.on('data', (data) => {
console.log('Received:', data);
});
emitter.emit('data', { value: 42 });
unsubscribe();
emitter.emit('data', { value: 100 }); // Won't log
function createClickCounter(element) {
let clicks = 0;
let lastClickTime = null;
element.addEventListener('click', function() {
clicks++;
lastClickTime = Date.now();
updateDisplay();
});
function updateDisplay() {
element.textContent = `Clicks: ${clicks}`;
}
function reset() {
clicks = 0;
lastClickTime = null;
updateDisplay();
}
function getStats() {
return {
clicks,
lastClickTime,
averageClickRate: lastClickTime ?
clicks / ((Date.now() - lastClickTime) / 1000) : 0
};
}
updateDisplay();
return {
reset,
getStats
};
}
const button = document.getElementById('myButton');
const counter = createClickCounter(button);
// Add reset button
const resetBtn = document.getElementById('reset');
resetBtn.addEventListener('click', () => counter.reset());
Now that you understand closures with event handlers, in the next lesson we'll explore how closures work with setTimeout and setInterval for managing timed operations.