A Bunch of Options for Looping Over querySelectorAll NodeLists

Avatar of Chris Coyier
Chris Coyier on (Updated on )

📣 Freelancers, Developers, and Part-Time Agency Owners: Kickstart Your Own Digital Agency with UACADEMY Launch by UGURUS 📣

A common need when writing vanilla JavaScript is to find a selection of elements in the DOM and loop over them. For example, finding instances of a button and attaching a click handler to them.

const buttons = document.querySelectorAll(".js-do-thing");
// There could be any number of these! 
// I need to loop over them and attach a click handler.

There are SO MANY ways to go about it. Let’s go through them.

forEach

forEach is normally for arrays, and interestingly, what comes back from querySelectorAll is not an array but a NodeList. Fortunately, most modern browsers support using forEach on NodeLists anyway.

buttons.forEach((button) => {
  button.addEventListener('click', () => {
    console.log("forEach worked");
  });
});

If you’re worried that forEach might not work on your NodeList, you could spread it into an array first:

[...buttons].forEach((button) => {
  button.addEventListener('click', () => {
    console.log("spread forEach worked");
  });
});

But I’m not actually sure if that helps anything since it seems a bit unlikely there are browsers that support spreads but not forEach on NodeLists. Maybe it gets weird when transpiling gets involved, though I dunno. Either way, spreading is nice in case you want to use anything else array-specific, like .map(), .filter(), or .reduce().

A slightly older method is to jack into the array’s natural forEach with this little hack:

[].forEach.call(buttons, (button) => {
  button.addEventListener('click', () => {
    console.log("array forEach worked");
  });
});

Todd Motto once called out this method pretty hard though, so be advised. He recommended building your own method (updated for ES6):

const forEach = (array, callback, scope) => {
  for (var i = 0; i < array.length; i++) {
    callback.call(scope, i, array[i]); 
  }
};

…which we would use like this:

forEach(buttons, (index, button) => {
  console.log("our own function worked");
});

for .. of

Browser support for for .. of loops looks pretty good and this seems like a super clean syntax to me:

for (const button of buttons) {
  button.addEventListener('click', () => {
    console.log("for .. of worked");
  });
}

Make an array right away

const buttons = Array.prototype.slice.apply(
  document.querySelectorAll(".js-do-thing")
);

Now you can use all the normal array functions.

buttons.forEach((button) => {
  console.log("apply worked");
});

Old for loop

If you need maximum possible browser support, there is no shame in an ancient classic for loop:

for (let i = 0; i < buttons.length; ++i) {
  buttons[i].addEventListener('click', () => {
    console.log("for loop worked");
  });
}

Wait! That example above has arrow functions and ES6 let. If you’re trying to go older and support old IE and such, you’ll have to…

for (var i = 0; i < buttons.length; ++i) {
  buttons[i].addEventListener('click', function() {
    console.log("for loop worked");
  });
}

Libraries

If you’re using jQuery, you don’t even have to bother….

$(".buttons").on("click", () => {
  console.log("jQuery works");
});

If you’re using a React/JSX setup, you don’t need think about this kind of binding at all.

Lodash has a _.forEach as well, which presumably helps with older browsers.

_.forEach(buttons, (button, key) => {
  console.log("lodash worked");
});

Poll

Twitter peeps:

Also here’s a Pen with all these options in it.