How to group array items with Object.groupBy()
By Flavio Copes
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.
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 covers those foundations first.
Group expenses by category
Start with a list of expenses:
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:
const byCategory = Object.groupBy(
expenses,
expense => expense.category,
)
The result looks like this:
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:
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:
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:
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 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:
const numbers = [1, 2, 3, 4, 5, 6]
const groups = Object.groupBy(numbers, number =>
number % 2 === 0 ? 'even' : 'odd'
)
The result is:
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:
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:
const products = [
{ name: 'Keyboard', inStock: true },
{ name: 'Display', inStock: false },
]
const byAvailability = Object.groupBy(
products,
product => product.inStock,
)
Access them as object properties:
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:
console.log(Object.keys(byCategory))
// ['food', 'transport']
console.log(Object.values(byCategory))
// [foodExpenses, transportExpenses]
Do not call inherited methods directly on the result:
byCategory.hasOwnProperty('food') // TypeError
Use Object.hasOwn() instead:
Object.hasOwn(byCategory, 'food') // true
If a library expects a regular object, copy the groups:
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:
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:
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.
const byCategory = Object.groupBy(
expenses,
expense => expense.category,
)
console.log(byCategory.office) // undefined
Use a fallback when a group might not exist:
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().
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.
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:
console.log(byTeam.get(design))
// Alice and Carol
Map keys use identity. Two separate objects with the same fields are different keys:
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.
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:
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:
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:
const sortedGroups = Object.entries(byCategory)
.toSorted(([a], [b]) => a.localeCompare(b))
You can also sort each group without changing it:
const cheapestFood = byCategory.food.toSorted(
(a, b) => a.amount - b.amount,
)
Read the immutable array methods for more copying operations.
Build a grouped view
A common use is preparing data for a UI.
Start with transactions:
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:
const byMonth = Object.groupBy(
transactions,
transaction => transaction.month,
)
Turn the object into display entries and sort the months:
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:
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:
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.
Related posts about js: