CodeNewbie Community 🌱

Cover image for Another way to understand JavaScript's array.reduce
Ben Holmes
Ben Holmes

Posted on • Originally published at dev.to

Another way to understand JavaScript's array.reduce

If you've run the guantlet of array methods in JavaScript, you've probably hit this roadblock a few times:

Wait, how do I use the reduce function again?

I actually led a JS bootcamp with this topic for my college's Hack4Impact chapter (material 100% free-to-use here!). Questions on reduce have come up so many times, and I think I've finally found an explanation that clicks 😁 Hope it works for you too!

🎥 Video walkthrough

If you prefer to learn by video tutorial, this one's for you. You can fork this CodePen for the source material to follow along 🏃‍♂️

📝 Step-by-step cheatsheet

Let's walk our way to reduce by using what we know: good ole' for loops.

Here's an example. Say we have our favorite album on a CD (remember those? 💿), and our stereo tells us the length of each track in minutes. Now, we want to figure out how long the entire album is.

Here's a simplified approach for what we want to do:

// make a variable to keep track of the length, starting at 0
let albumLength = 0
// walk through the songs on the album...
album.songs.forEach(song => {
  // and add the length of each song to our running total
  albumLength += song.minutesLong
})
Enter fullscreen mode Exit fullscreen mode

No too bad! Just loop over the songs, and accumulate the album runtime while we walk through the songs. This is basically the process you'd use in real life, tallying up the album length as you skip through the tracks on your stereo.

That word "accumulate" is pretty significant here though. In essence, we're taking this list of track lengths, and reducing them to a single accumulated number: the albumLength. This process of reducing to an accumulator should set off a light bulb in your head: 💡 we can use array.reduce!

Going from forEach to reduce

Let's try reduce-ifying our function from earlier. This is a simple, 4 step process:

  1. Change forEach to reduce:
let albumLength = 0
album.songs.reduce((song) => {
  albumLength = albumLength + song.minutesLong
})
Enter fullscreen mode Exit fullscreen mode
  1. Move albumLength to the first parameter of the loop function, and the initial value (0) to the second parameter of reduce
// accumulator up here 👇
album.songs.reduce((albumLength, song) => {
  albumLength = albumLength + song.minutesLong
}, 0) // 👈 initial value here
Enter fullscreen mode Exit fullscreen mode
  1. Change albumLength = to a return statement. This isn't too different conceptually, since we're still adding our song length onto our "accumulated" album length:
album.songs.reduce((albumLength, song) => {
  // 👇 Use "return" instead of our = assignment
  return albumLength + song.minutesLong
}, 0)
Enter fullscreen mode Exit fullscreen mode
  1. Retrieve the result of our reduce loop (aka our total album length). This is just the value returned:
const totalAlbumLength = album.songs.reduce((albumLength, song) => {
  return albumLength + song.minutesLong
}, 0)
Enter fullscreen mode Exit fullscreen mode

And that's it! 🎉

So wait, why do I even need reduce?

After all that work, reduce might feel like a slightly harder way of writing a for loop. In a way... it kind of is 😆

It offers one key benefit though: since reduce returns our total, function chaining is a lot easier. This may not be a benefit you appreciate right away, but consider this more complex scenario:

// Say we have this array of arrays,
// and we want to "flatten" everything to one big array of songs
const songsByAlbum = [
  ['Rap Snitches Knishes', 'Beef Rap', 'Gumbo'],
  ['Accordion', 'Meat Grinder', 'Figaro'],
  ['Fazers', 'Anti-Matter', 'Krazy World']
]
let songs = []
songsByAlbum.forEach(albumSongs => {
  // "spread" the contents of each array into our big array using "..."
  songs = [...songs, ...albumSongs]
})
Enter fullscreen mode Exit fullscreen mode

This isn't too hard to understand. But what if we want to do some more fancy array functions on that list of songs?

// Ex. Make these MF DOOM songs titles all caps
let songs = []
songsByAlbum.forEach(albumSongs => {
  songs = [...songs, ...albumSongs]
})
const uppercaseSongs = songs.map(song => song.toUppercase())
Enter fullscreen mode Exit fullscreen mode

MF DOOM giving finger guns

All caps when you spell the man name. Rest in piece MF DOOM

This is fine, but what if we could "chain" these 2 modifications together?

// grab our *final* result all the way at the start
const uppercaseSongs = [
  ['Rap Snitches Knishes', 'Beef Rap', 'Gumbo'],
  ['Accordion', 'Meat Grinder', 'Figaro'],
  ['Fazers', 'Anti-Matter', 'Krazy World']
]
// rewriting our loop to a "reduce," same way as before
.reduce((songs, albumSongs) => {
  return [...songs, ...albumSongs]
}, [])
// then, map our songs right away!
.map(song => song.toUppercase())
Enter fullscreen mode Exit fullscreen mode

Woah! Throwing in a reduce, we just removed our standalone variables for songsByAlbum and songs entirely 🤯

Take this example with a grain of salt though. This approach can hurt the readability of your code when you're still new to these array functions. So, just keep this reduce function in your back pocket, and pull it out when you could really see it improving the quality of your code.

Thanks for reading! If this post helped you...

I love writing about this sort of stuff. With this, I'm officially 5 posts into my goal to post once a week in 2021! Go ahead and drop a follow to hold me accountable 😁

You can also 🐦 find me on Twitter for tens of tweets of month, and learn more about ❤️ Hack4Impact here!

Top comments (5)

Collapse
 
r002 profile image
Robert Lin • Edited on

Hi Ben! Thanks so much for sharing this article on reduce(...). I've been recently trying to embrace a more "functional" style of program and write code where I avoid mutating any variables (killing off "standalone variables" as you call them). Reduce is certainly one helpful tool in the toolkit!

Out of curiosity, I'm wondering about your thoughts about putting a reduce(...) inside another reduce(...). Recently, I've been generating a lot of HTML code from JS (I'm trying to avoid React, at least for now, just for learning purposes) and I find myself using reduce all the time to collapse arrays of objects into a single HTML string to render. So for example, to generate an "Article List" GUI widget (screenshot) I've written: .../ts/widgets.ts#L8-L33.

At work, or elsewhere, do you see this kind of usage of reduce often? Is it a bad "code smell"? Any thoughts or opinions?

Thank you for writing this article up! 🙏 Map/Filter/Reduce is the way! ✊

Collapse
 
bholmesdev profile image
Ben Holmes • Edited on

Hey thanks Robert! That's a totally fair question. I've def seen nested reduce loops before, but it can be difficult to read if you throw everything together without helper functions.

So this is a little sketchy:

const songsByAlbum = [
  ['Rap Snitches Knishes', 'Beef Rap', 'Gumbo'],
  ['Accordion', 'Meat Grinder', 'Figaro'],
  ['Fazers', 'Anti-Matter', 'Krazy World']
]
songsByAlbum.reduce((songsAsString, album) => {
  // Flatten our album strings to a big string of songs across *all* albums
  return songsAsString + album.reduce((albumAsString, song) => {
    // Flatten album of songs to a string of songs
    return albumAsString + " " + song
  }, "")
}, "")
Enter fullscreen mode Exit fullscreen mode

...but this is a little better!

const flattenToSongs = (songs) => {
  return songs.reduce((songsAsString, song) => {
    return songsAsString + song
  }, "")
}

songsByAlbum.reduce((songsAsString, album) => {
  return songsAsString + flattenToSongs(album)
}, "")
Enter fullscreen mode Exit fullscreen mode

That said, you sometimes don't even need reduce for "stringifying" lists. We could totally do something like this instead:

songsByAlbum.map(album => {
  return album.join(" ") // join our list into a string separated by spaces
}).join(" ") // join this list of "album strings" to a big string
Enter fullscreen mode Exit fullscreen mode

PS: if you're using a nested reduce because you have arrays within arrays, there's a new helper in JS to "flatten" to a single array! It's called array.flat. More on that here

Collapse
 
r002 profile image
Robert Lin • Edited on

Very cool, thanks for sharing, Ben! I never knew about array.flat until today. Very neat. 👍

Everyday, I seem to find a new use for array.reduce though. It's seriously the gift that just keeps giving! 😄 Just this morning, I was searching for an equivalent to Python's counter data structure in JS and found this:

var arr = [5, 5, 5, 2, 2, 2, 2, 2, 9, 4]

const map = arr.reduce((acc, e) => acc.set(e, (acc.get(e) || 0) + 1), new Map())

console.info([...map.keys()])
console.info([...map.values()])
console.info([...map.entries()])
Enter fullscreen mode Exit fullscreen mode

What a beauty! What a time to be alive!! 🚀

Source: stackoverflow.com/a/57028486

Thread Thread
 
bholmesdev profile image
Ben Holmes

Haha yeah, love the enthusiasm 😁

Agreed, reduce is for a lot more than just reduce-ing arrays to something smaller (like a number of a string). That's why some languages like Rust and Kotlin call it "fold." That's because you're "folding" something of one data type (like an array) into something of another data type (like a Map in your example). Doesn't mean we've reduced it, we've just changed it into something different!

Collapse
 
eleloi profile image
eleloi

thank you Ben! Good explanation, this is the way