Expressing the Power of JavaScript .reduce method through simple explanations
There was a time when I was fairly new to JavaScript where the .reduce
method was hard to understand, its true potential was even harder to grasp. I have been reading about it, but no one was actually showing what can be achieved with low effort with what is now one of my favorite JavaScript methods!
As the name suggests, when using this method you express an intention to reduce a set of data into a single value. Let’s dig into some code examples but first of all a quick link to its documentation . In some of the examples you might see me use new (not so much now) JavaScript features like arrow functions =>
and spread syntax ...
, I assume you are fairly familiar with them, if not here spread syntax is explained and here are arrow functions.
.reduce
is a method that can be called on Arrays, it takes two arguments:
- A function: called once for every element in the array, it helps determine the final value that is returned from the
.reduce
operation (as just mentioned, we want to go from a set of data to a single value). What this means is that every time this function is called, a return statement determines what the final value should be, based on: the current item, the final value we have so far. Let’s see an example:
const fruits = ['banana', 'coconut', 'strawberry', 'apple'];const longestFruit = fruits.reduce(
(longestFruitThisFar, curFruit) => {
if (curFruit.length > longestFruitThisFar.length) {
return curFruit;
}
return longestFruitThisFar;
}
);// longestFruit is now 'strawberry'
The naming of variables should help you understand what is going on. Given an Array of fruits, I want to find the one with the longest name. Let’s break down each iteration of the .reduce
method:
longestFruitThisFar
isbanana
🍌, the simple reason being that when.reduce
calls the function the first time, the first parameter will correspond to the first value in the array (although we can specify our own, more on this later), the second one will be the next value in thefruits
Array, in this casecoconut
. At this point we are wondering ifcoconut
is longer thanbanana
, if that’s the case we use a return statement to let ourreduce
method know that our new final value iscoconut
.longestFruitThisFar
iscoconut
, this is coming from our previous iteration where we decided thatcoconut
was longer thanbanana
. At this pointcurFruit
should have the value of the next element in the Array which isstrawberry
. Againstrawberry
wins and is now our newlongestFruitThisFar
.longestFruitThisFar
isstrawberry
, again this is coming from our previous iteration where we returnedstrawberry
. Now we are going to compare it with the next value which isapple
, unfortunatelyapple
is too short (theif
statement is now false), sostrawberry
wins and is the returned value.
At this point we have scanned the whole array, and the last value returned is strawberry
, this means that longestFruit
is now officially strawberry
🍓! This is a very typical use for .reduce
, the beauty is that in the function we pass as a first parameter (the one where we do the comparisons), we can have any kind of logic we need from a very simple < or > comparison to more intricate logic. To be honest this is already enough to make your code do wonderful things ✨, but since we want to do even more, let’s keep digging and explore what the second argument is for.
- The second argument is the initial value, this is optional but is fundamental for certain use cases (we will see more examples on this). Following our example, instead of starting our
.reduce
withbanana
as thelongestFruitThisFar
inthe first iteration, we could have had (let’s say) a custom value, maybe one coming from user input. Let’s see it in action:
// Let's assume the user passed in 'orange' because he/she wants to
// know if that fruit is longer than any other we already have.
// Such value is saved in the variable named userFruit.const fruits = ['banana', 'coconut', 'strawberry', 'apple'];const longestFruit = fruits.reduce(
(longestFruitThisFar, curFruit) => {
if (curFruit.length > longestFruitThisFar.length) {
return curFruit;
}
return longestFruitThisFar;
},
userFruit,
);
The behavior would be similar to the one we have seen so far but, instead of having banana
as longestFruitThisFar
in the first iteration, we would have orange
(the value passed by the user) as the first longestFruitThisFar
, in this iterationcurFruit
would be banana
, and the same logic would be applied as we did before, iterating through the whole array. In this case strawberry
would win again, but at least we gave the user a chance 😉
If you are comfortable with what expressed so far, we can go further and explore more possibilities, otherwise I would recommend to come here later and practice what we have learned. You can try to use .reduce
to find the maximum value in an array of numbers, or sum all of the numbers in an array.
DIGGING A LITTLE DEEPER
What I think makes .reduce
a method with special powers 💪, is its flexibility. If you have used JavaScript for a while you should be familiar with other widely used methods such as .map
and .filter
. Not only you can easily replace them with .reduce
but you can replace many combinations of them! You heard it right!
Do you remember when I mentioned that using an initial value (the optional argument) could lead you to great things ? Now we can finally see why. The .reduce
method can not only be used to reduce a set of data to a single value, but also to build a new set of data given a set of data (whoooah that’s confusing). What if, at each iteration, instead of replacing or creating a new returned value (which eventually will be the final value) we add something to it ?
Let’s see an example for each of these cases, the mindset should be the following: given an array I want to build a new array where every element is a different version of its original form (again quite confusing 🤔). Some code will help clarifying this:
// Let's use .reduce to replace .map, given an array of fruits
// I want to map each item to its uppercase version. In other
// words I want to build a new array with uppercase versions
// of fruits in the original array.const fruits = ['banana', 'coconut', 'strawberry', 'apple'];const uppercaseFruits = fruits.reduce(
(myNewUppercaseFruits, curFruit) => {
return myNewUppercaseFruits.concat(curFruit.toUpperCase());
},
[],
);
I will explore only the first iteration. Did you notice that we are passing an empty array to use as initial value ? Do you remember what that entails in the first iteration ? You should know the answer by now, if not it’s alright “repetita iuvant”.
On my first iteration myNewUppercaseFruits
is an empty array, curFruit
is banana
. As a return value we grab the upper case fruits we have so far (in this case it’s just an empty array) and add the uppercase version of banana
. At this point myNewUpperCaseFruits
will be simply ['BANANA']
. Each other iteration will read the next item in the fruits
array and put its uppercase version in our myNewUppercaseFruits
. At the end we will have ['BANANA', 'COCONUT', 'STRAWBERRY', 'APPLE']
.
Do we wan to try replace .filter
with .reduce
? Why not!
// Let's use .reduce to replace .filter, given an array of fruits
// I want to filter only the ones that are longer than 5 letters.
// In other words I want to build a new array that only has
// fruits with a length greater than 5.const fruits = ['banana', 'coconut', 'strawberry', 'apple'];const longerFruits = fruits.reduce(
(myNewLongerFruits, curFruit) => {
if (curFruit.length > 5) {
return myNewLongerFruits.concat(curFruit);
}
return myNewLongerFruits;
},
[],
);
Easy as that! At each iteration I try to add a fruit only if its length is greater than 5, otherwise I don’t add anything and move along. So starting from an empty array (the optional second argument of .reduce
), at each iteration I return a new version of such array that includes what I added so far, plus (and this is done through the .concat
method) another fruit if the condition is satisfied.
I feel like by now you already know where I’m going with this 😜. If I wanted to uppercase every fruit and then filter only the ones whose length is greater than 5, intuitively I would use a .map
and then a .filter
or vice versa. So I would loop through my whole array twice! But do I really need to ? The answer is no! But HOW? Here you go:
// Let's use .reduce to replace .filter AND .map, given an array of // fruits I want to filter only the ones that are longer than 5
// letters and uppercase them.
// In other words I want to build a new array that only has
// fruits with a length greater than 5 whose name is now uppercase!const fruits = ['banana', 'coconut', 'strawberry', 'apple'];const longerUppercaseFruits = fruits.reduce(
(myNewLongerUppercaseFruits, curFruit) => {
if (curFruit.length > 5) {
return myNewLongerUppercaseFruits
.concat(curFruit.toUpperCase());
}
return myNewLongerUppercaseFruits;
},
[],
);
This is amazing! So what we learned so far is that we can replace .map
with .reduce
, we can replace .filter
with .reduce
but most importantly we can replace many combinations of .map
and .filter
with a single .reduce
!
This method is so powerful and embraces many functional aspects (maybe I will write about its functional aspects in more detail in another article). If I did not loose you along the way I hope you learned a lot from it, sometimes getting comfortable with .reduce
can be difficult at first but with some practice it will come to you naturally!
BONUS: Insert and Update with .reduce!
As a bonus I will show you another powerful use of .reduce
. Let’s say we have a cart of items, our goal is to add a new item to the cart only if we don’t have it already, otherwise we just update the quantity. Let’s roll:
const cart = [
{ name: 'banana', quantity: 5 },
{ name: 'cheese', quantity: 2 },
{ name: 'bread', quantity: 1 },
];// Let's assume the user wants to add 2 bananas and 1 watermelon,
// I would expect his/her items to be represented in the form of
// an array of objects just like our cart.
const userItems = [
{ name: 'banana', quantity: 2 },
{ name: 'watermelon', quantity: 1 },
];// Now I want to update the bananas quantity and add a watermelon
// I can do that with a simple .reduce
const updatedCart = userItems.reduce(
(newCart, curItem) => {
const curItemIndex = newCart
.findIndex(i => i.name === curItem.name); if (curItemIndex >= 0) {
return [
...newCart.slice(0, curItemIndex),
{
name: curItem.name,
quantity: curItem.quantity + newCart[curItemIndex].quantity,
},
...newCart.slice(curItemIndex + 1),
];
} return [...newCart, curItem];
},
cart,
);
I have seen around many other examples of an insert/update that were very hard to read. Once you are comfortable with .reduce
this feels more natural (and functional since we don’t have side effects or mutations). The way I read this is: starting with a cart (our optional second parameter), go through each item of the user (which is the array we are running our .reduce
on), try to find such item in the existing cart (using .findIndex
), if I find it then just update its quantity, if not then add it to the cart.
There are plenty other great uses of .reduce
in the wild out there, probably much more interesting than adding stuff to a cart or playing with fruits 😄! I wanted to explore the power and beauty of such method with you and make it easier to get a grasp on what you can achieve with it. I am by no means a master of JavaScript but I wish I had more pointers in the past that I could have leveraged in my web apps to make my code easier to understand. If you find something not accurate in the article let me know and I’ll try my best to make it right. If some of the concepts are still hard to understand leave a comment, I can always revisit the article to make it more clear.
This article follows my new resolution for 2018 to share what I learned and along the way learn something new myself. It’s the first more technical post so I hope you are not too disappointed, if I helped even one person that would make my day!
Now go ahead and have fun with .reduce
, feel free to share all the wonderful things you were able to do with it! Happy coding!