Group array items by a computed key

The problem

Repeated `find` and `filter` calls over the same list turn one cheap load into a slow render, and hand-written accumulator objects tend to collide on keys that need normalising first.

Short answer

Use Object.groupBy with a key function, or a single reduce pass when you need an insertion order guarantee or a Map for non-string keys.

Grouping is a two-line change that removes an accidental quadratic loop.

Language: JavaScript
const orders = [
  { id: 'a1', customer: 'acme', total: 120, placedAt: '2026-09-02' },
  { id: 'b7', customer: 'acme', total: 80, placedAt: '2026-09-05' },
  { id: 'c3', customer: 'northwind', total: 410, placedAt: '2026-09-11' },
];

const byCustomer = Object.groupBy(orders, (order) => order.customer);
// { acme: [ {a1}, {b7} ], northwind: [ {c3} ] }

Language: JavaScript
// When the key is not a string, or the caller needs the groups in a known order,
// accumulate in a Map instead.
function groupBy(list, keyOf, sortItems) {
  const groups = new Map();
  for (const item of list) {
    const key = keyOf(item);
    const bucket = groups.get(key);
    if (bucket) bucket.push(item);
    else groups.set(key, [item]);
  }
  if (sortItems) for (const bucket of groups.values()) bucket.sort(sortItems);
  return groups;
}

const byMonth = groupBy(
  orders,
  (order) => order.placedAt.slice(0, 7),
  (left, right) => left.total - right.total,
);

Explanation

Object.groupBy calls the callback once per element and appends the element to the array stored under the returned key, so a pass that would otherwise need a filter per group becomes a single traversal. The callback’s return value is converted with ToPropertyKey, meaning numbers and booleans become usable keys, and a returned null or undefined becomes the literal string keys "null" and "undefined" rather than a missing bucket. That conversion is the most common source of surprise: order.customerId of 0 and '0' land in the same group.

The returned object has a null prototype. That protects you from a key like __proto__ or constructor colliding with inherited members — the ordinary accumulator bug where a grouping key named toString silently reads a function instead of a bucket — but it also means the result has no hasOwnProperty, so use Object.hasOwn(groups, key) and prefer Object.entries or Object.keys when you iterate.

The Map variant exists for the cases Object.groupBy cannot express: keys that must stay objects or numbers without string coercion, group order that must follow first insertion rather than key order, and per-group sorting done in the same pass. It also keeps the accumulator reusable when the caller supplies the comparator.

Parameters

  • list (array, required): records to group.
  • keyOf (function, required): returns the key for one record; called once per element.
  • sortItems (function, optional): comparator applied inside each group after accumulation.

Expected output

Object.groupBy yields an object whose values are arrays of the original references, in input order. The Map helper yields the same arrays keyed by the raw key value.

Common mistakes

  • Reaching for reduce with a {} seed and no prototype protection when keys come from user data.
  • Assuming a key of 0 stays numeric; string coercion means the lookup key must also be coerced.

Caveats

  • Object.groupBy requires a browser generation from 2024 onward; the Map helper runs anywhere.
  • Grouping keeps references to the original items, so a group array is not a snapshot of the record.

Examples that treat this as a prerequisite

Derived at build time from the editorial graph; no edge is invented here.

Related examples

Editorial links first, then deterministic same-task or same-topic candidates. Tags and shared language alone never qualify a candidate.

References