Understanding Javascript Array's .reduce() method and its use-cases
After reading various tweets and blogs, I decided to write a detailed article on what exactly reduce method is and what can be some of its use cases.
The .reduce()
method was also there in ES5, but I'd never used or seen someone using it in production at that time. Even today, I see people not aware of this method. I've also seen people only giving the same shopping cart example
while explaining this method and, that's why I think people don't completely understand it. I feel this is the reason this method is underrated though it is powerful.
When I encountered this for the first time, I was happy with the regular for loop for doing the shopping cart calculation. I didn't feel like using it for such tasks. But after digging deep into this method, I realized I could do many more things.
After reading various tweets and blogs, I decided to write a detailed article on what exactly reduce method is and what can be some of its use cases. We won't be talking about the speed of regular for loop and .reduce() because the for loop is way faster than any other Array methods, be it reduce
, map
, filter
, sort
, forEach
, etc.
If you have any real-world use cases on your mind, do let me know. I'll add those examples in this article with the attribution.
Understanding .reduce()
We can use .reduce()
on an array and it executes a reducer function (a callback function) on each element and, we can define the logic of this reducer function. This .reduce() also accepts a second argument: initialValue
.
arr.reduce((accumulator, currentElement, currentIndex, array) => {
// your logic goes here...
}, initialValue);
This reducer function (callback) takes 4 arguments:
accumulator
: It accumulates all the values returned by the callback function. That means, at the end of the callback function, whatever value you return, it becomes the value of the accumulator for the next iteration. If you pass theinitialValue
, it becomes the value of the accumulator for the first iteration. Else, it takes the first value of the array in its first iteration.currentElement
: It holds the value of the current element of the array that is being processed in the current iteration. If we don't pass theinitialValue
it starts with the 2nd element of the array because the accumulator takes the 1st element.currentIndex
: It holds the index (position) of thecurrentElement
in the array. If there is noinitialValue
, it starts with index 1, because that's the position of the currentElement in this case.array
: This contains the original array on which we're executing the reduce method. You might need this array in some cases where you're importing the callback function from a different file and that file doesn't have any reference/context of the original array.
Note: If we don't return any value at the end of the function, in the next iteration, the value of the accumulator becomes undefined
.
As per the documentation, the first 2 arguments (accumulator and currentElement) are required but we can execute this function without passing any arguments (which is useless). All the other arguments are optional.
Let's take a simple example to understand this in detail
const array = [1, 2, 3, 4, 5];
const value = array.reduce((acc, curr, i, arr) => {
acc += curr;
// Here curr = arr[i];
return acc;
// If we don't return any value,
// The accumulator becomes undefined in the next iteration
}, 0);
console.log(value); // 15
In this example, we've passed the initial value: 0
, of the accumulator. That's why in the first iteration:
- The value of the accumulator becomes
0
. - CurrentElement holds the value
1
. (The first element of the array) - Because the current element holds the first value, its index becomes
0
.
If we remove the initial value, the accumulator will hold the first element of the array: 1
, the current element will hold the second element: 2
and the index will start from 1
.
But what about ES5? There are no arrow functions...
In ES5, we can replace the fat arrow function with the function
keyword and, everything else stays the same.
array.reduce(function(acc, curr, i, arr) {
// Your Logic
}, initialValue);
Now let's see some of its use-cases
Here, in this section, we'll see how and where we can use .reduce()
. The intention of this section is not to provide the optimal solution but to explore new use-cases.
If you're concerned about the execution speed, DO NOT use reduce
, map
, forEach
, etc. because they all are way slower than the original for loop
.
Replicating .map() using .reduce()
const array = [1, 2, 3, 4, 5];
const data = array.reduce((acc, curr) => {
// Your logic on `curr`
acc.push(curr);
return acc;
}, []);
console.log(data); // [1, 2, 3, 4, 5]
In this example, after getting the value of curr
, we can do any operation on it and push the old/new value in the accumulator and return it.
Replicating .filter() using .reduce()
Now, let's try to filter an array of numbers and get all the numbers greater than 0
using reduce.
const array = [-3, -2, -1, 0, 1, 2, 3];
const filtered = array.reduce((acc, curr) => {
if (curr > 0) {
acc.push(curr);
}
return acc;
}, []);
console.log(filtered); // [1, 2, 3]
Removing duplicate entries from the array
It's extremely easy to remove duplicate entries from an array using .reduce()
. The main advantage of using this method is that it'd work even in ES5, unlike Set
or some other ES6+ specific ways.
const array = [1, 2, 1, 2, 3, 1, 4, 5, 4];
const unique = array.reduce((acc, curr) => {
if (acc.indexOf(curr) === -1) {
acc.push(curr);
}
return acc;
}, []);
console.log(unique); // [1, 2, 3, 4, 5]
Flattening all the items from an array
If we have an array that contains let's say numbers and the array of numbers, and we want all the values from the array, we can easily flatten an array using .reduce()
.
I've faced this while calling some APIs that if there's only 1 element, it returns a string. Otherwise, it returns an array of strings.
const array = [[1, 2, 3], 4, 5, [6, 7]];
const flatten = array.reduce((acc, curr) => {
if (Array.isArray(curr)) {
acc.push(...curr);
} else {
acc.push(curr)
}
return acc;
}, []);
console.log(flatten); // [1, 2, 3, 4, 5, 6, 7]
We can also write this callback function in 1 line using the ternary operator, but for the sake of understanding, I've kept the if/else condition.
Converting an Array into an Object and counting the occurrences
This is a very common use case, and most of the time, I see people using "lodash" or similar libraries to perform such small tasks. I don't like to bloat my project with a lot of libraries that I barely use once or twice, and can easily be replaced by a few lines of Vanilla JS code.
This can be one of the simplest example of counting the number of occurrences of each item in your array:
const array = ['๐', '๐', '๐', '๐', '๐', '๐'];
const obj = array.reduce((acc, curr) => {
if (acc[curr]) {
acc[curr] += 1;
} else {
acc[curr] = 1;
}
return acc;
}, {});
console.log(obj); // {๐: 2, ๐: 3, ๐: 1}
Chaining .reduce() with .map()
Is it possible that we talk about .reduce()
and not mention everyone's favorite shopping cart
example?
But here, let's try something new. Now after understanding all these examples, everyone knows that in the example given below, we can pass 0
as the initial value of the accumulator and return acc + curr.price
.
But did you know, you can also chain map and reduce? I'm sure after looking at the example, you would think that this is obvious, but at least we tried something new! Also, this degrades the performance even more. ๐คท๐ป
const items = [
{
item: '๐',
price: 10,
},
{
item: '๐',
price: 20,
},
{
item: '๐',
price: 15,
},
];
const total = items
.map(item => item.price)
.reduce((acc, curr) => acc + curr);
console.log(total); // Output: 45
Converting an Array of Objects to hash map
Wait a second, I'm not talking about the new ES6 feature: Map
. Although we can achieve the same thing using Map
but there are certain advantages and disadvantages of using Map
over an Object
.
Now, let's say you have an array of objects like this:
const arr = [
{ key: 'Tom', value: 'Jerry' },
{ key: 'Mario', value: 'Luigi' },
{ key: 'SpongeBob', value: 'Patrick' },
];
And, you want a key pair like this:
{
"Tom": "Jerry",
"Mario": "Luigi",
"SpongeBob": "Patrick",
}
You can simply achieve this using .reduce()
like in the example given below. Isn't it extremely simple and straightforward?
const keyPair = arr.reduce((acc, curr) => {
acc[curr.key] = curr.value;
return acc;
}, {});
Okay, now that we discussed about Map
, let's not leave the discussion there. Let's see how we can achieve the same thing in ES6's: Map
. Here, we are not discussing which one is better because it depends on the use-case. We may discuss this in a separate blog. But for now let's enjoy this example.
Here, we'll use .map()
to create a Map
!!!
const keyPair = new Map(arr.map(
item => [item.key, item.value]
));
console.log(keyPair); // {"Tom" => "Jerry", "Mario" => "Luigi", "SpongeBob" => "Patrick"}
Conclusion
In this article, our main focus was to understand and explore the different use-cases of .reduce()
. Of course, it is slower than regular for loop
but for relatively smaller objects/arrays, you won't even feel the difference (it also depends on the task you're performing on each element).
For the same reason, you can choose to use reduce
, map
, forEach
etc. in the frontend because the browser has to run this code for only 1 client. But I would not recommend this in the backend because there are limited machines and they've to handle a lot of requests. Even if you can save 10 ms
for a large object, it may make a huge difference while handling a lot of requests simultaneously. In such cases, you should go for the regular for loop. Even after optimizing your code from every corner you feel that the code is still slow, it's time to switch the language! (C/C++ maybe!)
Do let me know on Twitter (and follow if possible ๐) if you have any other use-cases on your mind which you use in production or you might've thought something out of the box. I would love to address them and include them in this article.
Credits
Cover Image: https://morioh.com/