Simplify, Groupify, Conquer: The convenience of Object.GroupBy() in JavaScript

Simplify, Groupify, Conquer: The convenience of Object.GroupBy() in JavaScript

·

6 min read

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 iterableis the element to be grouped

  • The callbackFunctionis 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 callbackFunctionaccepts elementand indexas its arguments
    where,

  • The element represents the current element which is being passed to the callbackFunction at the moment and the index is the index of the element 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.

Browser compatibility of Object.groupBy() method

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.

developer.mozilla.org

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.

developer.mozilla.org