DEV Community

Cover image for A guide to Object.groupBy: An alternative to Array.reduce
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

A guide to Object.groupBy: An alternative to Array.reduce

Written by Sebastian Weber✏️

Sorting a list by a shared category is a common task in JavaScript, often solved using [Array.prototype.reduce](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce). While powerful, reduce is a bit cumbersome and heavyweight for this kind of job. For years, the use of this functional programming approach was a common pattern for converting data into a grouped structure.

Enter [Object.groupBy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy), a new utility that has gained cross-browser compatibility as of the end of 2024.

Designed to simplify the grouping process of data structures, Object.groupBy offers a more intuitive and readable way to group and sort lists by a shared category. In this article, we’ll compare the functional approach of reducing with the new grouping method, explore how they differ in implementation, and provide insights into performance considerations when working with these tools.

How reduce works

The reduce method is a powerful utility for processing arrays. The term "reducer" originates from functional programming. It's a widely used synonym for "fold" or "accumulate."

In such a paradigm, reduce represents a higher-order function that transforms a data structure (like an array) into a single aggregated value. It reduces, so to speak, a collection of values into one value by repeatedly applying a combining operation, such as summing numbers or merging objects.

The signature looks like this:

reduce<T, U>(
  callbackFn: (accumulator: U, currentValue: T, currentIndex: number, array: T[]) => U,
  initialValue: U
): U;
// signature of callback function
callbackFn: (accumulator: U, currentValue: T, currentIndex: number, array: T[]) => U
Enter fullscreen mode Exit fullscreen mode

Let's break down the different parts:

  • accumulator: The aggregated result from the previous callback execution or the initialValue for the first execution. It has the type of the initial value (type U)
  • currentValue: The current element of the array being processed (type T)
  • currentIndex: The index of the currentValue in the array (type number)
  • array: The array on which reduce was called (type T[])
  • **initialValue**: This sets the initial value of the accumulator (type U) if provided. Otherwise, the value is set to the first array item

After all array items are processed, the method returns a single value, i.e., the accumulated result of type U.

Let's pretend we have an array of order objects:

const orders = [
    { category: 'electronics', title: 'Smartphone', amount: 100 },
    { category: 'electronics', title: 'Laptop', amount: 200 },
    { category: 'clothing', title: 'T-shirt', amount: 50 },
    { category: 'clothing', title: 'Jacket', amount: 100 },
    { category: 'groceries', title: 'Apples', amount: 10 },
    // ...
];
Enter fullscreen mode Exit fullscreen mode

We want to group the order list into categories like this:

{
    electronics: [
        {
            category: "electronics",
            title: "Smartphone",
            amount: 100
        },
        {
            category: "electronics",
            title: "Laptop",
            amount: 200
        }
    ],
    clothing: [
        {
            category: "clothing",
            title: "T-shirt",
            amount: 50
        },
        {
            category: "clothing",
            title: "Jacket",
            amount: 100
        }
    ],
    // ...
}
Enter fullscreen mode Exit fullscreen mode

The next snippet shows a possible implementation:

const groupedByCategory = orders.reduce((acc, order) => {
    const { category } = order;
    // Check if the category key exists in the accumulator object
    if (!acc[category]) {
        // If not, initialize it with an empty array
        acc[category] = [];
    }
    // Push the order into the appropriate category array
    acc[category].push(order);
    return acc;
}, {});
Enter fullscreen mode Exit fullscreen mode

Simplified grouping with Object.groupBy

Let's compare the previous code to the following implementation with the new Object.groupBy static method:

const ordersByCategory = Object.groupBy(orders, order => order.category);
Enter fullscreen mode Exit fullscreen mode

This solution is straightforward to understand. The callback function in Object.groupBy must return a key for each element (order) in the passed array (orders).

In this example, the callback returns the category value since we want to group all orders by all unique categories. The created data structure looks exactly like the result of the reduce function.

To demonstrate that the callback can return any string, let's organize products into price ranges:

const products = [
    { name: 'Wireless Mouse', price: 25 },
    { name: 'Bluetooth Headphones', price: 75 },
    { name: 'Smartphone', price: 699 },
    { name: '4K Monitor', price: 300 },
    { name: 'Gaming Chair', price: 150 },
    { name: 'Mechanical Keyboard', price: 45 },
    { name: 'USB-C Cable', price: 10 },
    { name: 'External SSD', price: 120 }
  ];

const productsByBudget = Object.groupBy(products, product => {
    if (product.price < 50) return 'budget';
    if (product.price < 200) return 'mid-range';
    return 'premium';
});
Enter fullscreen mode Exit fullscreen mode

The value of productsByBudget looks like this:

{
    budget: [
        {
            "name": "Wireless Mouse",
            "price": 25
        },
        {
            "name": "Mechanical Keyboard",
            "price": 45
        },
        {
            "name": "USB-C Cable",
            "price": 10
        }
    ],
    "mid-range": [
        {
            "name": "Bluetooth Headphones",
            "price": 75
        },
        {
            "name": "Gaming Chair",
            "price": 150
        },
        {
            "name": "External SSD",
            "price": 120
        }
    ],
    premium: [
        {
            "name": "Smartphone",
            "price": 699
        },
        {
            "name": "4K Monitor",
            "price": 300
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Let's consider the following example:

const numbers = [1, 2, 3, 4];
const isGreaterTwo = Object.groupBy(numbers, x => x > 2);
Enter fullscreen mode Exit fullscreen mode

The value of isGreaterTwo looks like this:

{
    "false": [1, 2],
    "true": [3, 4]
}
Enter fullscreen mode Exit fullscreen mode

This demonstrates how Object.groupBy automatically casts non-string return values into string keys when creating group categories. In this case, the callback function checks whether each number is greater than two, returning a Boolean. These Booleans are then transformed into the string keys "true" and "false" in the resulting object.

N.B., remember that automatic type conversion is usually not a good practice.

Limitations of Object.groupBy

Object.groupBy excels at simplifying basic grouping operations, but has limitations. It directly places the exact array items into the resulting groups, preserving their original structure.

However, if you want to transform the array items while grouping, you'll need to perform an additional transformation step after the Object.groupBy operation. Here is a possible implementation to group orders but remove superfluous category properties:

const cleanedGroupedOrders = Object.fromEntries(
    Object.entries(Object.groupBy(myOrders, order => order.category))
        .map(([key, value]) => [key, value.map(groupedValue => (
            { 
                title: groupedValue.title, 
                amount: groupedValue.amount 
            }
        ))])
);
Enter fullscreen mode Exit fullscreen mode

Alternatively, you can utilize reduce with its greater flexibility for transforming data structures:

const cleanedGroupedOrders = orders.reduce((acc, order) => {
    const { category } = order;
    if (!acc[category]) {
        acc[category] = [];
    }
    acc[category].push({ title: order.title, amount: order.amount });
    return acc;
}, {});
Enter fullscreen mode Exit fullscreen mode

Group items into a map structure with Map.groupBy

If you need to group objects with the ability to mutate them afterwards, then [Map.groupBy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/groupBy) is most likely the better solution:

const groupedOrdersMap = Map.groupBy(orders, order => order.category);
Enter fullscreen mode Exit fullscreen mode

As you see, the API is the same: Group Array Items Into A Map Structure If you group array elements of primitive data types or read-only objects, then stick with Object.groupBy, which performs better.

JavaScript grouping performance: Object.groupBy vs. Map.groupBy vs. reduce

To compare the performance of these different grouping methods, I conducted a large list of order objects to perform various grouping algorithms:

const categories = ['electronics', 'clothing', 'groceries', 'books', 'furniture'];
const orders = [];
// Generate 15 million orders with random categories and amounts
for (let i = 0; i < 15000000; i++) {
    const category = categories[Math.floor(Math.random() * categories.length)];
    // Random amount between 1 and 500
    const amount = Math.floor(Math.random() * 500) + 1; 
    orders.push({ category, amount });
}
Enter fullscreen mode Exit fullscreen mode

With that test data in place, I leverage [performance.now](https://developer.mozilla.org/de/docs/Web/API/Performance/now) to measure the runtimes. I run the different variants 25 times on different browsers and calculate the mean value of the running times for each variant:

const runtimeData = {
  'Array.reduce': [],
  'Object.groupBy': [],
  'Map.groupBy': [],
  'reduce with transformation': [],
  'Object.groupBy + transformation': []
};
for (let i = 0; i < 25; i++) {
  console.log(`Run ${i + 1}`);
  measureRuntime();
}
// Log average runtimes
console.log('Average runtimes:');
for (const [variant, runtimes] of Object.entries(runtimeData)) {
  const average = runtimes.reduce((a, b) => a + b, 0) / runtimes.length;
  console.log(`${variant}: ${average.toFixed(2)} ms`);
}
function measureRuntime() {
  // Object.groupBy
  start = performance.now();
  const groupedOrdersGroupBy = Object.groupBy(orders, order => order.category);
  end = performance.now();
  runtimeData['Object.groupBy'].push(end - start);

  // Array.reduce
  // ...

  // Map.groupBy
  // ...

  // Reduce with transformation
  // ...

  // Object.groupBy + transformation
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Here are the performance results:

Method Chrome (ms) Firefox (ms) Safari (ms)
Array.reduce 443.32 262.04 1153.2
Object.groupBy 540.96 233.16 659.28
Map.groupBy 424.41 581.6 731.32
reduce with transformation 653.74 1125.6 1054.8
Object.groupBy with transformation 860.61 1993.92 1609.12

Based on these results, Object.groupBy was the fastest method on average, followed by Map.groupBy. However, if you need to apply additional transformations while grouping, reduce may be the better choice in terms of performance.

Browser support

Object.groupBy and Map.groupBy have reached baseline status as of the end of 2024. To check specific browser compatibility, you can refer to their respective CanIUse pages (Object.groupBy and Map.groupBy).

If you need to support older browsers, you can use a shim/polyfill.

Conclusion

When it comes to grouping data in JavaScript, choosing between the new data manipulation functions or Array.prototype.reduce depends on the level of flexibility and transformation you need.

Object.groupBy makes grouping more intuitive, but it’s limited if you need to modify data in the process. Map.groupBy offers a more flexible structure, especially for mutable data, while reduce remains the go-to method for complex or custom transformations. The choice between the methods depends on the specific requirements of your use case.


LogRocket: Debug JavaScript errors more easily by understanding the context

Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.

LogRocket Javascript Signup

LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

Try it for free.

Top comments (0)