It was a regular day of coding when I decided to revisit an old project of mine. As I scrolled through my code, I noticed a piece of code I had written during the earlier days of my JavaScript journey. The project was an Expense Tracker web application that allowed users to manually enter transactions and store them in LocalStorage. The specific code snippet I encountered was related to the grouping of data.
Back then, I remember grappling with the challenges of grouping data and attempting solutions using filter()
, map()
, and even forEach()
. However, to my surprise, I discovered that there was an actual method designed for conveniently grouping data.
A sample from the actual set of data to be handled is as follows:
const transacHistory = [
{ type: 'Income', desc: 'Salary', amount: '52000', date: '09/01/2023' },
{ type: 'Expense', desc: 'House rent', amount: '7500', date: '09/03/2023'},
{ type: 'Expense', desc: 'Shopping', amount: '8000', date: '09/11/2023' },
{ type: 'Income', desc: 'YouTube', amount: '3508', date: '09/12/2023' },
{ type: 'Expense', desc: 'Veggies', amount: '550', date: '9/02/2023' },
{ type: 'Expense', desc: 'Internet', amount: '2250', date: '09/11/2023' },
{ type: 'Expense', desc: 'Veggies', amount: '350', date: '9/18/2023' },
]
Now, I wanted to group them based on the type of transaction made ( expense or income )
The old-school data grouping technique
const groupedTransacHistory = transacHistory.reduce((acc, item)=>{
if(!Object.keys(acc).includes(item.type)){
acc[item.type] = [item]
}
else{
acc[item.type].push(item)
}
return acc;
}, [])
console.log(groupedTransacHistory)
{*
OUTPUT:
[
Income: [
{ type: 'Income', desc: 'Salary', amount: '52000', date: '09/01/2023' },
{ type: 'Income', desc: 'YouTube', amount: '3508', date: '09/12/2023' }
],
Expense: [
{ type: 'Expense', desc: 'House rent', amount: '7500', date: '09/03/2023' },
{ type: 'Expense', desc: 'Shopping', amount: '8000', date: '09/11/2023' },
{ type: 'Expense', desc: 'Veggies', amount: '550', date: '9/02/2023' },
{ type: 'Expense', desc: 'Internet', amount: '2250', date: '09/11/2023' },
{ type: 'Expense', desc: 'Veggies', amount: '350', date: '9/18/2023' }
]
]
*}
The code to achieve the above output might seem to be a bit complex, but anyone familiar with the filter()
method might understand the logic. If you wish to know more about JavaScript's filter()
method, refer to the official MDN docs embedded at the bottom of this article. The same logic can also be implemented using map()
and forEach()
methods as well. As their logic is similar, I've included only the code associated with the filter()
method.
Know more about the groupBy()
method:
Before letting you know the actual solution using the proposed groupBy()
method, let's analyse how to use that in the first place.
The Object.groupBy()
method is used to categorize individual items of an iterable. The categorization is done according to the coerced string values returned by the provided callback function. The grouped/categorized object thus returned contains unique keys with each key corresponding to an array of related values. Is it sound confusing? You are on the right path. Follow along to see this method in action.
SYNTAX
where,
The
iterable
is the element to be groupedThe
callbackFunction
is the function to be passed to execute each element in the iterable. This function should return a value which is then coerced into a string.
function callbackFunction(element, index){...}
The
callbackFunction
acceptselement
andindex
as its arguments
where,The
element
represents the current element which is being passed to thecallbackFunction
at the moment and theindex
is the index of theelement
itself.
Let's look at some general and simple examples to grasp the concept.
Examples
The following code groups items based on whether they start with a vowel or a consonant.
1. The traditional approach:
const vowels = ['a', 'e', 'i', 'o', 'u']
const cartItems = ['apples', 'bananas', 'cherries', 'umbrellas', 'cat food', 'pedigree', 'cupcakes']
const res2 = cartItems.reduce((accumulator, item) => {
if (vowels.includes(item.slice(0, 1))) {
if (!accumulator['vowels']) {
accumulator['vowels'] = [];
}
accumulator['vowels'].push(item);
} else {
if (!accumulator['non-vowels']) {
accumulator['non-vowels'] = [];
}
accumulator['non-vowels'].push(item);
}
return accumulator;
}, {});
{*
OUTPUT:
{
vowels: ['apples', 'umbrellas'],
non-vowels: ['bananas', 'cherries', 'cat food', 'pedigree', 'cupcakes']
}
*}
2. The Object.groupBy()
approach:
const vowels = ['a', 'e', 'i', 'o', 'u']
const cartItems = ['apples', 'bananas', 'cherries', 'umbrellas', 'cat food', 'pedigree', 'cupcakes']
Object.groupBy(cartItems, (item)=>vowels.includes(item.slice(0, 1)))
//item.slice(0,1) returns the first letter of the currently iterated item and checks whether it is a vowel.
{*
OUTPUT:
{
true: ['apples', 'umbrellas'],
false: ['bananas', 'cherries', 'cat food', 'pedigree', 'cupcakes']
}
*}
If there are no object properties to be displayed in the case of arrays, the Object.groupBy()
method simply uses the Boolean values as keys. If we need precisely set keys, then we should implement Map.groupBy()
method which is the replica of our former method but lets us set keys instead of the default Boolean.
const fictionalCharacters = [
{ character: 'Ironman', ability: 'Fights bad guys' },
{ character: 'Barbie', ability: "Doesn't fight" },
{ character: 'Black widow', ability: 'Fights bad guys' },
{ character: 'Oggy', ability: "Doesn't fight" },
{ character: 'Peppa Pig', ability: "Doesn't fight" }
]
Object.groupBy( fictionalCharacters, (item)=>item.ability )
{* The value of item.ability is type-converted into a string and used as a key to group similar items in arrays, where the key's value is an array of corresponding items. Example: { item.ability: [{}, {}] } *}
{*
OUTPUT:
[
Fights bad guys: [
{ character: 'Ironman', ability: 'Fights bad guys' },
{ character: 'Black widow', ability: 'Fights bad guys' },
],
Doesn't fight: [
{ character: 'Barbie', ability: "Doesn't fight" },
{ character: 'Oggy', ability: "Doesn't fight" },
{ character: 'Peppa Pig', ability: "Doesn't fight" }
]
]
*}
So, as now we have a pretty decent understanding of how it works, let's get back to our expense tracker example.
const transacHistory = [
{ type: 'Income', desc: 'Salary', amount: '52000', date: '09/01/2023' },
{ type: 'Expense', desc: 'House rent', amount: '7500', date: '09/03/2023'},
{ type: 'Expense', desc: 'Shopping', amount: '8000', date: '09/11/2023' },
{ type: 'Income', desc: 'YouTube', amount: '3508', date: '09/12/2023' },
{ type: 'Expense', desc: 'Veggies', amount: '550', date: '9/02/2023' },
{ type: 'Expense', desc: 'Internet', amount: '2250', date: '09/11/2023' },
{ type: 'Expense', desc: 'Veggies', amount: '350', date: '9/18/2023' },
]
Object.groupBy(transacHistory, (item)=>item.type)
{*
OUTPUT:
[
Income: [
{ type: 'Income', desc: 'Salary', amount: '52000', date: '09/01/2023' },
{ type: 'Income', desc: 'YouTube', amount: '3508', date: '09/12/2023' }
],
Expense: [
{ type: 'Expense', desc: 'House rent', amount: '7500', date: '09/03/2023' },
{ type: 'Expense', desc: 'Shopping', amount: '8000', date: '09/11/2023' },
{ type: 'Expense', desc: 'Veggies', amount: '550', date: '9/02/2023' },
{ type: 'Expense', desc: 'Internet', amount: '2250', date: '09/11/2023' },
{ type: 'Expense', desc: 'Veggies', amount: '350', date: '9/18/2023' }
]
]
*}
Simple as that. This is how the groupBy() method is convenient to use and understand.
NOTE:
Instead of making a huge leap to the static methods like Object.groupBy(), beginners should be comfortable using map(), filter() and reduce() methods.
The Object.groupBy() method, as of 17th Dec 2023, is not supported in all browsers and it is expected to be in the near future.
Refer to the following document to learn more about the filter() method in JavaScript:
Array.prototype.filter() - JavaScript | MDN
The filter() method of Array instances creates a shallow copy of a portion of a given array, filtered down to just the elements from the given array that pass the test implemented by the provided function.
To know more about Object.groupBy() method, refer to the following official docs by MDN
Object.groupBy() - JavaScript | MDN
The Object.groupBy() static method groups the elements of a given iterable according to the string values returned by a provided callback function. The returned object has separate properties for each group, containing arrays with the elements in the group.