Open any frontend codebase and search for .reduce. I'll wait.
A meaningful fraction of those hits look like this:
const byStatus = tasks.reduce((acc, task) => {
const key = task.status;
if (!acc[key]) acc[key] = [];
acc[key].push(task);
return acc;
}, {});
Or, if the author has been around long enough, this tighter variant:
const byStatus = tasks.reduce((acc, task) => {
(acc[task.status] ??= []).push(task);
return acc;
}, {});
Both do the same thing: group an array by a key. It's one of the most common data transforms in any frontend codebase — group tasks by status, events by date, errors by code, users by role. You've written it dozens of times because JavaScript never had a built-in name for it.
It does now. And it has been in every major browser since early 2024.
Object.groupBy — the one-liner
const byStatus = Object.groupBy(tasks, task => task.status);
Pass an iterable and a callback. The callback returns the group key for each item. The result is a plain object where each key holds an array of the items that mapped to it.
const tasks = [
{ id: 1, status: 'done', title: 'Ship it' },
{ id: 2, status: 'todo', title: 'Write tests' },
{ id: 3, status: 'done', title: 'Fix the bug' },
{ id: 4, status: 'in-progress', title: 'Review the PR' },
];
const grouped = Object.groupBy(tasks, t => t.status);
// {
// done: [{ id: 1, … }, { id: 3, … }],
// todo: [{ id: 2, … }],
// 'in-progress': [{ id: 4, … }],
// }
The callback can return any value that stringifies to a useful key — a status enum, a date string, a category slug, a computed bucket. Items that map to the same string land in the same array. Items that would map to undefined land under the key "undefined", which is a sign you should add a fallback in the callback.
🎮 Try it yourself
▶️ Open the interactive playground →
Runs right in your browser — poke at it and watch the concept react live.
When you need non-string keys: Map.groupBy
Object.groupBy coerces every key to a string. That covers most cases. If you need to group by something that isn't meaningfully stringifiable — an object reference, a boolean you want distinct from the string "true", or any value where identity matters — Map.groupBy is the variant:
const byPriority = Map.groupBy(tasks, task => task.priority > 5);
byPriority.get(true); // high-priority tasks
byPriority.get(false); // everything else
The result is a real Map, so you access values with .get() and iterate with .entries(). Keys are the exact values your callback returned — no stringification, no coercion, no surprises.
A real-world example: the status board
Before groupBy, building a Kanban-style board from a flat API response involved one of two patterns. Either a filter per lane:
const lanes = ['todo', 'in-progress', 'review', 'done'];
const grouped = Object.fromEntries(
lanes.map(status => [status, tasks.filter(t => t.status === status)])
);
That walks tasks once per lane — four passes for four columns. Or a reduce — one pass, but four lines of bookkeeping per developer who reads it.
With Object.groupBy:
const grouped = Object.groupBy(tasks, t => t.status);
const lanes = ['todo', 'in-progress', 'review', 'done'];
// `grouped` already has everything; `lanes` just controls render order
One pass. No ceremony. The intent is in the function name.
One subtle thing about the returned object
Object.groupBy returns an object with a null prototype — Object.create(null). It has no inherited toString, hasOwnProperty, or any of the usual Object.prototype methods. It's a pure data dictionary.
For most real uses this is exactly right: no prototype key collisions, no need for the hasOwnProperty dance before accessing a key. If downstream code needs a normal prototype — say, you're serializing it with a library that calls toString — wrap it: Object.assign({}, grouped). But in practice you rarely need to.
Support
Object.groupBy is Baseline 2024: Chrome 117, Firefox 119, Safari 17.4, Node.js 21. If your targets include browsers from the last two years you can use it today without a polyfill. For older targets a one-liner shim is trivial, but at this point you're more likely to just set a browserslist target that excludes the handful of users still on pre-2024 releases.
🧠 Test yourself
Think it clicked? Take the 6-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
The takeaway
Object.groupBy isn't a convenience — it's the word for what you've been spelling out in four lines of reduce. Find the grouping reduces in your codebase, replace them, and delete whatever groupBy helper you wrote to avoid writing them inline. The operation has a name and a native implementation now; there's nothing left to carry.
Top comments (4)
Use
flatMap()first, then run your reducer (or groupBy) function on the new Map.That's a nice approach. 👍
Using
flatMap()first makes the transformation pipeline much easier to read by separating the flattening step from the grouping step:I like this style when the intermediate flattened array is useful or when readability is the priority. If I'm working with a very large dataset and want to avoid creating that extra array, I'd probably stick with a single-pass reducer instead.
Thanks for sharing—it's a clean alternative that fits well in many real-world cases.
How does Object.groupBy handle nested objects, I've had issues with reduce in those cases. Would love to hear your thoughts on this.
Great question! In my experience,
Object.groupBy()actually handles nested objects just as well asreducebecause the grouping logic is entirely driven by the callback you provide.For example, grouping by a nested property is as straightforward as:
Where
reduceoften becomes harder to maintain is when you're simultaneously flattening data, checking for missing nested properties, and building the grouped object by hand.Object.groupBy()lets you focus just on the grouping criteria, so the intent is much clearer.The main limitation is that
Object.groupBy()groups by string or symbol keys. If you need to group by object references or other non-string values,Map.groupBy()is usually the better fit.Have you mostly run into issues with deeply nested properties, or with handling missing/optional values along the way?