2HAASStart a project
All posts

Code Snippets

Group JavaScript Data with Object.groupBy and Map.groupBy

Learn how to efficiently group array elements into objects or maps using the new Object.groupBy() and Map.groupBy() methods in modern JavaScript.

The 2HAAS team6 min read

Effectively organizing data is a fundamental task in software development. Developers often face the challenge of transforming a flat list of items into a structured collection, grouped by a specific characteristic. Historically, achieving this in JavaScript required writing repetitive boilerplate code, typically involving Array.prototype.reduce() or explicit loops, which could quickly become complex and error-prone for intricate grouping logic.

The Challenge of Manual Grouping

Before the introduction of standardized grouping methods, developers commonly used Array.prototype.reduce() to aggregate elements into a new object or map. While powerful, this approach demands careful management of the accumulator and often results in verbose code, especially when dealing with nested structures or conditional grouping. For instance, grouping a list of users by their city would look something like this:

const users = [
  { id: 1, name: 'Alice', city: 'New York' },
  { id: 2, name: 'Bob', city: 'London' },
  { id: 3, name: 'Charlie', city: 'New York' },
  { id: 4, name: 'David', city: 'Paris' },
];

const usersByCityOld = users.reduce((acc, user) => {
  const key = user.city;
  if (!acc[key]) {
    acc[key] = [];
  }
  acc[key].push(user);
  return acc;
}, {});

console.log(usersByCityOld);
// Output:
// {
//   'New York': [ { id: 1, name: 'Alice', city: 'New York' }, { id: 3, name: 'Charlie', city: 'New York' } ],
//   'London': [ { id: 2, name: 'Bob', city: 'London' } ],
//   'Paris': [ { id: 4, name: 'David', city: 'Paris' } ]
// }

This code is functional, but it requires several lines to set up the accumulator, check for existing keys, and push new items. As grouping logic becomes more complex, so does the reduce callback.

Introducing Object.groupBy()

ECMAScript 2024 (ES14) introduces Object.groupBy() as a more declarative and streamlined way to group elements of an iterable. This static method takes two arguments:

  1. An iterable: The collection of elements you want to group (e.g., an array).
  2. A callbackFn: A function that receives each element and its index, and returns the key by which that element should be grouped.

Object.groupBy() returns a plain JavaScript object where the keys are the stringified return values of your callbackFn, and each value is an array of elements that share that key. It's ideal when your grouping keys are naturally strings or can be easily coerced into strings.

Let's revisit our users example using Object.groupBy():

const users = [
  { id: 1, name: 'Alice', city: 'New York' },
  { id: 2, name: 'Bob', city: 'London' },
  { id: 3, name: 'Charlie', city: 'New York' },
  { id: 4, name: 'David', city: 'Paris' },
];

const usersByCityNew = Object.groupBy(users, user => user.city);

console.log(usersByCityNew);
// Output (same as above, but with much less code):
// {
//   'New York': [ { id: 1, name: 'Alice', city: 'New York' }, { id: 3, name: 'Charlie', city: 'New York' } ],
//   'London': [ { id: 2, name: 'Bob', city: 'London' } ],
//   'Paris': [ { id: 4, name: 'David', city: 'Paris' } ]
// }

This dramatically reduces boilerplate and improves readability. Any value returned by the callbackFn (except undefined) will be coerced to a string for the object key. If undefined is returned, the element is grouped under the key 'undefined'.

When to Use Map.groupBy()

While Object.groupBy() is perfect for most scenarios, JavaScript object keys are always strings or Symbols. What if your grouping key needs to be a non-primitive value, like another object or a function, and you need to preserve its original type? This is where Map.groupBy() comes in.

Map.groupBy() functions identically to Object.groupBy() in terms of arguments and purpose, but it returns a Map instance instead of a plain object. The key advantage of a Map is that it allows any value (including objects, arrays, or null) to be used as a key, preserving its type and identity.

Consider grouping a list of events by their exact Date objects:

const events = [
  { id: 1, name: 'Meeting', date: new Date('2024-07-20') },
  { id: 2, name: 'Workshop', date: new Date('2024-07-21') },
  { id: 3, name: 'Presentation', date: new Date('2024-07-20') },
];

// Using Map.groupBy to preserve Date object keys
const eventsByDate = Map.groupBy(events, event => event.date);

console.log(eventsByDate);
// Output:
// Map(
//   [Date 2024-07-20T00:00:00.000Z] => [ { id: 1, name: 'Meeting', date: [Date] }, { id: 3, name: 'Presentation', date: [Date] } ],
//   [Date 2024-07-21T00:00:00.000Z] => [ { id: 2, name: 'Workshop', date: [Date] } ]
// )

If you tried to do this with Object.groupBy(), the Date objects would be coerced into their string representations, which might lead to unintended grouping if Date objects with different identities but identical string forms were used as keys. Map.groupBy() ensures key identity is preserved.

Practical Use Cases

These new grouping methods are incredibly versatile and can simplify many common programming tasks:

  • Data Aggregation: Easily group financial transactions by month, product sales by region, or sensor readings by device ID.
  • UI Rendering: Prepare data for displaying grouped lists in user interfaces, such as grouping emails by sender or files by type.
  • Categorization: Organize items into categories for filtering or navigation, like grouping books by genre or recipes by cuisine.
  • Complex Grouping Logic: The callbackFn can contain arbitrary logic, allowing you to group by calculated properties, multiple conditions, or even results from asynchronous operations (if you structure your code carefully).

For example, grouping a list of orders by their status and then by the customer ID:

const orders = [
  { orderId: 'A001', customerId: 'C1', status: 'pending' },
  { orderId: 'A002', customerId: 'C2', status: 'shipped' },
  { orderId: 'A003', customerId: 'C1', status: 'shipped' },
  { orderId: 'A004', customerId: 'C3', status: 'pending' },
];

const ordersByStatus = Object.groupBy(orders, order => order.status);

// To group further, you would apply groupBy again on each status group.
const groupedAndNested = {};
for (const status in ordersByStatus) {
  groupedAndNested[status] = Object.groupBy(ordersByStatus[status], order => order.customerId);
}

console.log(JSON.stringify(groupedAndNested, null, 2));
/* Output:
{
  "pending": {
    "C1": [
      { "orderId": "A001", "customerId": "C1", "status": "pending" }
    ],
    "C3": [
      { "orderId": "A004", "customerId": "C3", "status": "pending" }
    ]
  },
  "shipped": {
    "C2": [
      { "orderId": "A002", "customerId": "C2", "status": "shipped" }
    ],
    "C1": [
      { "orderId": "A003", "customerId": "C1", "status": "shipped" }
    ]
  }
}
*/

Browser Support and Considerations

Object.groupBy() and Map.groupBy() are relatively new additions to the JavaScript specification (ES2024). As of mid-2024, they are widely supported in modern browsers like Chrome (117+), Firefox (119+), Safari (17.0+), and Node.js (20.0+). For environments that do not yet support these methods, you may need to use polyfills or continue with the reduce pattern. Always check compatibility tables (e.g., MDN Web Docs or caniuse.com) for your target environments.

Keep in mind that if your callbackFn returns null, the elements will be grouped under the key 'null'. Similarly, undefined results in the key 'undefined'. Ensure your grouping logic explicitly handles edge cases like null or missing properties if these values should be treated specially.

Takeaway

Object.groupBy() and Map.groupBy() are powerful new tools that significantly enhance JavaScript's data manipulation capabilities. They allow developers to write cleaner, more expressive, and less error-prone code when grouping iterable elements. By choosing the appropriate method—Object.groupBy() for string-coercible keys and Map.groupBy() for maintaining key type identity—you can streamline your data processing workflows and improve code maintainability. Embrace these new features to make your JavaScript code more modern and efficient.

Keep reading

  1. Code Snippets4 min read

    Simplify Promise Handling with Promise.withResolvers

    Learn how the new Promise.withResolvers static method streamlines common asynchronous patterns in JavaScript, reducing boilerplate and improving code clarity.

  2. Tech News5 min read

    TypeScript 5.5 Beta Enhances Developer Experience

    Learn about the new features in TypeScript 5.5 Beta. Discover how improved control flow analysis and type inference enhance your coding.

  3. Code Snippets4 min read

    Simplify Popups with HTML Popover API

    Learn how to effortlessly create accessible popups, tooltips, and modals using the native HTML Popover API, reducing custom JavaScript and improving user experience.

Tell us what’s in the way.

A few lines is enough. You get a candid yes, a candid no, or a sharper question.

  • Working software in week four
  • A reply in one business day
  • Handover is the deliverable

Prefer email? [email protected]

The problem, who it is for, and what is in the way right now.

One business day to a real reply. No sales sequence, no call required.