# How to group array items with Object.groupBy()

> Group an array by a property with Object.groupBy() instead of a reduce loop. See a realistic expense example and Map.groupBy() for object keys.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-08-22 | Updated: 2026-08-03 | Topics: [JavaScript](https://flaviocopes.com/tags/js/) | Canonical: https://flaviocopes.com/javascript-object-groupby/

`Object.groupBy()` splits items into named groups.

You pass it an iterable and a callback. The callback returns the group key for each item. The result contains one array for every key.

This replaces a common `reduce()` pattern with a method that says exactly what the code does.

If callbacks, objects, and iterables are new to you, the [free JavaScript course](https://flaviocopes.com/courses/javascript/) covers those foundations first.

## Group expenses by category

Start with a list of expenses:

```js
const expenses = [
  { name: 'Coffee', category: 'food', amount: 4 },
  { name: 'Bus pass', category: 'transport', amount: 45 },
  { name: 'Lunch', category: 'food', amount: 12 },
  { name: 'Train ticket', category: 'transport', amount: 30 },
]
```

Group them by the `category` property:

```js
const byCategory = Object.groupBy(
  expenses,
  expense => expense.category,
)
```

The result looks like this:

```js
const byCategory = {
  food: [
    { name: 'Coffee', category: 'food', amount: 4 },
    { name: 'Lunch', category: 'food', amount: 12 },
  ],
  transport: [
    { name: 'Bus pass', category: 'transport', amount: 45 },
    { name: 'Train ticket', category: 'transport', amount: 30 },
  ],
}
```

The callback runs once for every item. Its return value decides which array receives that item.

The callback also receives the current index:

```js
const groups = Object.groupBy(
  expenses,
  (expense, index) => index < 2 ? 'first-half' : 'second-half',
)
```

Most grouping rules only need the item. Use the index when position is genuinely part of the rule.

Unlike array methods such as `map()`, the callback does not receive the original iterable as a third argument.

## It works with any iterable

The first argument does not have to be an array. `Object.groupBy()` accepts an iterable.

For example, group values from a `Set`:

```js
const temperatures = new Set([12, 18, 24, 31])

const byRange = Object.groupBy(temperatures, temperature => {
  if (temperature < 15) return 'cold'
  if (temperature < 25) return 'mild'
  return 'hot'
})
```

Each group in the returned object is still an array.

This makes the method useful with sets, generator results, and other iterable data sources.

## The old reduce() pattern

Before `Object.groupBy()`, you usually wrote this:

```js
const byCategory = expenses.reduce((groups, expense) => {
  const key = expense.category

  if (!groups[key]) {
    groups[key] = []
  }

  groups[key].push(expense)
  return groups
}, {})
```

This is valid JavaScript. It also mixes several operations:

- choose the key
- check whether the group exists
- create the group
- add the item
- return the accumulator

`Object.groupBy()` handles that bookkeeping. Keep `reduce()` for cases where the output is not a set of item arrays. Read [JavaScript reduce](https://flaviocopes.com/javascript-reduce/) if you want to understand the original pattern.

## Group by a calculated value

The key does not need to come directly from a property.

Group numbers into odd and even buckets:

```js
const numbers = [1, 2, 3, 4, 5, 6]

const groups = Object.groupBy(numbers, number =>
  number % 2 === 0 ? 'even' : 'odd'
)
```

The result is:

```js
const groups = {
  odd: [1, 3, 5],
  even: [2, 4, 6],
}
```

You can group dates by year, orders by price range, or messages by sender. The callback only needs to return a key.

## Group by more than one condition

You can turn several values into one label:

```js
const orders = [
  { id: 1, paid: true, shipped: true },
  { id: 2, paid: true, shipped: false },
  { id: 3, paid: false, shipped: false },
]

const byStatus = Object.groupBy(orders, order => {
  if (!order.paid) return 'awaiting-payment'
  if (!order.shipped) return 'ready-to-ship'
  return 'shipped'
})
```

The callback is a good place to name a business rule. Keep it focused on choosing the group. Do not also mutate the item there.

## Keys become property keys

`Object.groupBy()` returns an object, so normal keys become strings.

This code returns groups named `true` and `false`:

```js
const products = [
  { name: 'Keyboard', inStock: true },
  { name: 'Display', inStock: false },
]

const byAvailability = Object.groupBy(
  products,
  product => product.inStock,
)
```

Access them as object properties:

```js
console.log(byAvailability.true)
console.log(byAvailability.false)
```

Symbols can also be keys. Objects cannot remain object keys in a plain object. Use `Map.groupBy()` when key identity matters.

## The result has a null prototype

`Object.groupBy()` returns a null-prototype object.

That means it does not inherit properties such as `toString` and `constructor`. A group named `toString` cannot collide with `Object.prototype.toString`.

You can use the normal static object methods:

```js
console.log(Object.keys(byCategory))
// ['food', 'transport']

console.log(Object.values(byCategory))
// [foodExpenses, transportExpenses]
```

Do not call inherited methods directly on the result:

```js
byCategory.hasOwnProperty('food') // TypeError
```

Use `Object.hasOwn()` instead:

```js
Object.hasOwn(byCategory, 'food') // true
```

If a library expects a regular object, copy the groups:

```js
const regularObject = { ...byCategory }
```

## Transform grouped items afterward

`Object.groupBy()` groups the original values. It does not transform them.

If you only need expense names, map each group after grouping:

```js
const namesByCategory = Object.fromEntries(
  Object.entries(byCategory).map(([category, items]) => [
    category,
    items.map(item => item.name),
  ]),
)
```

If you need totals instead of arrays, `reduce()` is more direct:

```js
const totals = expenses.reduce((result, expense) => {
  result[expense.category] ??= 0
  result[expense.category] += expense.amount
  return result
}, {})
```

Grouping and aggregation are related, but they are not the same operation.

## Missing groups do not get empty arrays

Only keys returned by the callback appear in the result.

```js
const byCategory = Object.groupBy(
  expenses,
  expense => expense.category,
)

console.log(byCategory.office) // undefined
```

Use a fallback when a group might not exist:

```js
const officeExpenses = byCategory.office ?? []
```

Do not add every possible empty group unless the consumer needs that exact shape. A UI can often render the groups that exist with `Object.entries()`.

```js
for (const [category, items] of Object.entries(byCategory)) {
  console.log(category, items.length)
}
```

If a fixed set of keys is part of the application contract, initialize that structure explicitly before or after grouping.

## Use Map.groupBy() for object keys

`Map.groupBy()` returns a `Map`. It can keep objects as keys.

```js
const design = { id: 1, name: 'Design' }
const backend = { id: 2, name: 'Backend' }

const people = [
  { name: 'Alice', team: design },
  { name: 'Bob', team: backend },
  { name: 'Carol', team: design },
]

const byTeam = Map.groupBy(people, person => person.team)
```

Read a group with the original object:

```js
console.log(byTeam.get(design))
// Alice and Carol
```

Map keys use identity. Two separate objects with the same fields are different keys:

```js
const first = { id: 1 }
const second = { id: 1 }

console.log(first === second) // false
```

Use `Object.groupBy()` for string-like labels such as status, category, or year. Use `Map.groupBy()` when you need objects or other values to remain distinct keys.

## Grouped objects are shared

`Object.groupBy()` creates the group object and its arrays. It does not clone each item.

```js
const products = [
  { name: 'Keyboard', category: 'accessories', price: 90 },
  { name: 'Mouse', category: 'accessories', price: 40 },
]

const grouped = Object.groupBy(
  products,
  product => product.category,
)
```

Change an object through a group:

```js
grouped.accessories[0].price = 80

console.log(products[0].price) // 80
```

Both arrays point to the same product object.

If you need independent values, copy them before grouping:

```js
const grouped = Object.groupBy(
  products.map(product => ({ ...product })),
  product => product.category,
)
```

Only add that copy when independence is required. Shared unchanged objects are normal and avoid unnecessary work.

## Sort the groups or their contents

Object property order is not a sorting feature. Turn the groups into entries when display order matters:

```js
const sortedGroups = Object.entries(byCategory)
  .toSorted(([a], [b]) => a.localeCompare(b))
```

You can also sort each group without changing it:

```js
const cheapestFood = byCategory.food.toSorted(
  (a, b) => a.amount - b.amount,
)
```

Read [the immutable array methods](https://flaviocopes.com/javascript-immutable-array-methods/) for more copying operations.

## Build a grouped view

A common use is preparing data for a UI.

Start with transactions:

```js
const transactions = [
  { id: 1, month: '2026-07', amount: 30 },
  { id: 2, month: '2026-08', amount: 12 },
  { id: 3, month: '2026-07', amount: 45 },
]
```

Group them by month:

```js
const byMonth = Object.groupBy(
  transactions,
  transaction => transaction.month,
)
```

Turn the object into display entries and sort the months:

```js
const sections = Object.entries(byMonth)
  .toSorted(([firstMonth], [secondMonth]) =>
    secondMonth.localeCompare(firstMonth)
  )
  .map(([month, items]) => ({
    month,
    items,
    total: items.reduce((sum, item) => sum + item.amount, 0),
  }))
```

Grouping separates the collections. The following `map()` adds view-specific data without changing the original transactions.

This is a good boundary: use `groupBy()` for membership, then use other methods for sorting and aggregation.

## Common mistakes

The first mistake is returning an object from the `Object.groupBy()` callback:

```js
const grouped = Object.groupBy(people, person => person.team)
```

If `person.team` is an object, it is converted to a string key such as `'[object Object]'`. Different teams can collapse into the same group.

Use `Map.groupBy()` when object identity is the key.

The second mistake is calling inherited methods on the null-prototype result. Use `Object.keys()`, `Object.entries()`, and `Object.hasOwn()`.

The third mistake is using grouping when you only need totals. Returning arrays and reducing them afterward does more work than one direct `reduce()`.

The fourth mistake is mutating items inside the grouping callback:

```js
const grouped = Object.groupBy(expenses, expense => {
  expense.amount = Math.round(expense.amount)
  return expense.category
})
```

This changes the original objects as a side effect of choosing a key. Transform first, then group, or transform the grouped values afterward.

## When to use groupBy()

Use `Object.groupBy()` when the result should be several arrays:

- posts grouped by tag
- orders grouped by status
- transactions grouped by month
- people grouped by team

Use `reduce()` when you need totals, averages, nested indexes, or another custom result.

My advice is to choose the method that names the operation. If you are grouping, `Object.groupBy()` is clearer than rebuilding the same accumulator every time.

`Object.groupBy()` and `Map.groupBy()` work in current browsers and modern Node.js releases. Check compatibility when you support older runtimes.
