Learn how to write a custom group by function in JavaScript by using Array.prototype.reduce
.
After learning about the upcoming Array.prototype.group
feature in JavaScript, I couldn’t help but want to create a streamlined version to use now.
Here’s what I came up with, using Array.prototype.reduce
as the base, to achieve the same result in a minimalistic way.
Read my article on Grouping Arrays of Objects in JavaScript with the
Array.prototype.group
method.
Let’s take the following data structure, take note of the type
property:
const data = [
{ type: 'food', name: 'Pizza' },
{ type: 'drink', name: 'Coffee' },
{ type: 'food', name: 'Hot Dog' }
];
With the Array.prototype.group
method, it gives us a new object with each type
value as a key, followed by an array of the relevant elements:
const { food, drink } = data.group((item) => item.type);
// ✅ [{ type: 'food', name: 'Pizza' }, { type: 'food', name: 'Hot Dog' }]
console.log(food);
// ✅ [{ type: 'drink', name: 'Coffee' }]
console.log(drink);
So how could we do it? There are of course multiple ways, it might be that we could use something like Array.prototype.filter
and then compose an object from the results of each call.
However, that would require multiple loops which would be far less optimized to handling larger data structures.
First, let’s start with our function definition to emulate the native prototype method:
const group = (items, fn) => {};
group(data, (item) => item.type);
That’s our API footprint out of the way, what next?
const group = (items, fn) => {
return items.reduce((prev, next) => {
const prop = fn(next);
}, {});
};
I’ve added a callback fn
argument, which takes the first value of our array and passes it to our callback to get the property, the next
value.
From here, this will be type
which we’ll use to look up the items in the array.
Now for the crazy part, composing the object. You’ll notice the starting value of our reduce
is actually an object {}
on the last line. This means we’ll start with this object and slowly build it to contain our new values, using the chosen type
property.
First we’ll merge in any existing values, which allows us to keep reusing our object, and secondly add a dynamic property setter from our prop
to set the new value:
const group = (items, fn) => {
return items.reduce((prev, next) => {
const prop = fn(next);
return {
...prev,
[prop]: undefined
};
}, {});
};
This gives us { food: undefined, drink: undefined }
, so now we need to fill in the goodness.
Next we need to create an array result for each property, so let’s start with that and ‘append’ the current item in the loop to it:
const group = (items, fn) => {
return items.reduce((prev, next) => {
const prop = fn(next);
return {
...prev,
[prop]: [next],
};
}, {});
};
But at this point, on every iteration of the loop, the result would be lost because we are setting the value each time.
We need to merge any existing values into an array value, or set a new one:
const group = (items, fn) => {
return items.reduce((prev, next) => {
const prop = fn(next);
return {
...prev,
[prop]: prev[prop] ? [...prev[prop], next] : [next],
};
}, {});
};
If the prev[prop]
exists, set the array with all the current values, and the next
value. Otherwise, set the array to just the next
value to initialize it.
And that’s it! We could then clean things up with some implicit return statements and done:
const group = (items, fn) => items.reduce((prev, next) => {
const prop = fn(next);
return {
...prev,
[prop]: prev[prop] ? [...prev[prop], next] : [next],
};
}, {});
This won’t be as performant as Array.prototype.group, but that’s not available in browsers just yet without a polyfill.
🏆 P.S. check out my latest JavaScript courses if you’re serious about taking your skills to the top.
Thanks for reading, happy grouping!