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
Let's break down the different parts:
-
accumulator
: The aggregated result from the previous callback execution or theinitialValue
for the first execution. It has the type of the initial value (typeU
) -
currentValue
: The current element of the array being processed (typeT
) -
currentIndex
: The index of thecurrentValue
in the array (type number) -
array
: The array on which reduce was called (typeT[]
) -
**initialValue**
: This sets the initial value of the accumulator (typeU
) 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 },
// ...
];
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;
}, {});
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);
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.
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
}
))])
);
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;
}, {});
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);
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.
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 });
}
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
// ...
}
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 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!
Top comments (0)