Skip to main content Accessibility Feedback

Why you shouldn't attach event listeners in a for loop with vanilla JavaScript

Whenever I write about event delegation, I mention that you shouldn’t attach event listeners in a for loop (I also mention this in my pocket guide on DOM manipulation).

To add the same event listener to multiple elements, you also cannot just use a for loop because of how the i variable is scoped (as in, it’s not and changes with each loop).

I’ve had a few people tell me I haven’t explained this particularly well, so today, I wanted to explain this a bit more clearly.

You actually can attach event listeners in a for loop

As you can see from this example, an event listener is successfully attached to each button, and the console is logged each time one of them is clicked.

<button>One</button>
<button>Two</button>
<button>Three</button>
var btns = document.querySelectorAll('button');
for (var i = 0; i < btns.length; i++) {
	btns[i].addEventListener('click', function (event) {
		console.log('clicked');
	}, false);
}

Problems emerge when you try to use your i counter variable within the event callback.

The i variable will always be the last element

In this updated example, I log the current button the loop to the console. When clicked, I’ll try to log that same button again by using both the btns[i] and event.target references.

var btns = document.querySelectorAll('button');
for (var i = 0; i < btns.length; i++) {
	console.log('1', btns[i]);
	btns[i].addEventListener('click', function (event) {
		console.log('2', btns[i]);
		console.log('3', event.target);
	}, false);
}

You can see in the demo that the buttons initially log fine, and event.target works, but btns[i] logs undefined.

Why is that?

The i variable isn’t scoped to the loop

After each iteration of the loop, the i variable increases by 1.

The value does not remain constant within the scope of your event listener callback function. It changes.

After the last iteration of the loop, i is increased by 1 one last time. When our i < btns.length check runs, it fails because i is bigger than the number of items in our node list, and the loop ends.

When you try to log btns[i] in the event listener, it’s try to reference an index that doesn’t exist in the node list.

So we’re cool as long as we don’t use i in the event listener?

Technically, yes. But you still shouldn’t do it.

I think the danger of accidentally doing so is too great, and as a best practice, would recommend avoiding setting event listeners this way.

It’s also worse for performance.

It’s more performant to have a single event listener that listens for all clicks and check if the element has a selector you want than it is to attach a bunch of individual listeners.