Object.groupBy
: An alternative to Array.reduce
Sorting a list by a shared category is a common task in JavaScript, often solved using Array.prototype.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
, 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.
reduce
worksThe 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
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 itemAfter 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 }, // ... ];
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 } ], // ... }
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; }, {});
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);
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'; });
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 } ] }
Let’s consider the following example:
const numbers = [1, 2, 3, 4]; const isGreaterTwo = Object.groupBy(numbers, x => x > 2);
The value of isGreaterTwo
looks like this:
{ "false": [1, 2], "true": [3, 4] }
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.
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 } ))]) );
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; }, {});
Map.groupBy
If you need to group objects with the ability to mutate them afterwards, then Map.groupBy
is most likely the better solution:
const groupedOrdersMap = Map.groupBy(orders, order => order.category);
As you see, the API is the same:
If you group array elements of primitive data types or read-only objects, then stick with Object.groupBy
, which performs better.
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 }); }
With that test data in place, I leverage 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 // ... }
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.
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.
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.
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 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!
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowGet a high-level comparison of five of the most popular and well-used CI/CD tools for React Native apps, including the features they support.
API Mocking allows frontend developers to simulate the responses and behaviors of a live API, such as error handling, timeouts, and specific status codes in real time. Chrome DevTools Local Overrides make this even easier.
Enhance navigation in multi-page apps using the View Transition API, CSS, and JavaScript — no heavy frameworks needed.
Developers can take advantage of the latest release of .NET MAUI 9 to build better user experiences that more accurately react and respond to changes in their applications.