Section 3 · Working with Data
Summing and Sorting: reduce and sort
18 min4 min readCode playground
Lesson at a glance
Summarise a list into one value with reduce, build per-category totals, and sort numbers, text and objects correctly with a comparator, without the default-sort and mutation traps.
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
Two questions every budgeting app has to answer: “how much did I spend on food?” and “what were my biggest expenses?”. The first turns a list into one number per category. The second puts the list in a useful order. JavaScript has a method for each, and each has one famous trap.
reduce: many values into one
You already know the accumulator pattern: start with a value, update it for each item, use it at the end. reduce is that pattern as a method:
const expenses = [
{ label: "Coffee", amount: 450, category: "food" },
{ label: "Train", amount: 1220, category: "travel" },
{ label: "Lunch", amount: 1350, category: "food" },
];
const total = expenses.reduce((sum, e) => sum + e.amount, 0);
console.log(total);reduce takes two arguments: a callback and an initial value (here 0). It calls the callback once per item with the current accumulator (sum) and the item (e). Whatever the callback returns becomes the accumulator for the next call. After the last item, that final accumulator is the result.
Always pass the initial value. Without it, reduce uses the first item as the starting accumulator, which here would be a whole expense object rather than a number, and on an empty list it throws TypeError: Reduce of empty array with no initial value.
Grouping: reduce into an object
The accumulator can be anything, including an object. That is how you build per-category totals:
const expenses = [
{ label: "Coffee", amount: 450, category: "food" },
{ label: "Train", amount: 1220, category: "travel" },
{ label: "Lunch", amount: 1350, category: "food" },
];
const byCategory = expenses.reduce((totals, e) => {
totals[e.category] = (totals[e.category] ?? 0) + e.amount;
return totals;
}, {});
console.log(byCategory);For each expense, read the category’s running total (or 0 if this is the first time the category appears; the ?? operator gets its own explanation next lesson), add the amount, and store it back with bracket notation, since the property name is in a variable. Then return the same object so the next call receives it.
When you want the expenses themselves grouped, rather than a total per group, there is a built-in shortcut since 2024: Object.groupBy(expenses, (e) => e.category) returns an object whose properties are the categories and whose values are arrays of the matching expenses. It is supported in all current browsers and Node.js, and it reads better than the equivalent reduce. Totals still need reduce (or a loop over the groups).
reduce is powerful, and that is a reason to use it with restraint. If a map, filter or a short for...of says the same thing more plainly, prefer it. Totals and grouping are where reduce reads best.
sort: putting things in order
Sorting looks easy and contains the most famous trap in JavaScript:
const amounts = [450, 1220, 99];
amounts.sort();
console.log(amounts);Without instructions, sort converts every item to a string and orders them as text, like words in a dictionary. As text, “1220” comes before “450” because the character “1” comes before “4”. That is fine for words and wrong for numbers.
The fix is a comparator: a function that receives two items, a and b, and returns a number that says which comes first.
| Comparator returns | Result |
|---|---|
| a negative number | a goes before b |
| a positive number | b goes before a |
0 |
keep their current order |
For numbers, subtraction produces exactly that:
const amounts = [450, 1220, 99];
console.log([...amounts].sort((a, b) => a - b)); // ascending
console.log([...amounts].sort((a, b) => b - a)); // descendingWhen a is 450 and b is 99, a - b is positive, so 99 moves first. Flip it to b - a for biggest first.
Text needs its own comparison, because < compares character codes and puts every uppercase letter before every lowercase one. localeCompare compares the way a dictionary in a real language does:
const labels = ["lunch", "Train", "coffee", "Éclair"];
console.log(labels.toSorted((a, b) => a.localeCompare(b)));Objects are sorted by one of their properties, using the same patterns: (a, b) => b.amount - a.amount for most expensive first, (a, b) => a.label.localeCompare(b.label) for alphabetical.
sort changes the original
sort rearranges the array in place and returns that same array. If other code is reading the list, perhaps to show expenses in the order they were added, it is now sorted too. That is why the examples above sort a copy, [...amounts]. The newer toSorted method (part of JavaScript since 2023 and supported by all current browsers) returns a sorted copy directly, so you do not have to remember the copy step:
const expenses = [
{ label: "Coffee", amount: 450 },
{ label: "Train", amount: 1220 },
{ label: "Lunch", amount: 1350 },
];
const biggestFirst = expenses.toSorted((a, b) => b.amount - a.amount);
console.log(biggestFirst.map((e) => e.label), expenses.map((e) => e.label));Next you deal with data that is not there at all: missing properties, empty values, and two collections, Map and Set, built for lookups and uniqueness.
Key takeaways
reducewalks a list carrying an accumulator; the callback returns the next accumulator, and the last one is the result.- Always pass
reducean initial value, such as0for totals or{}for grouping; without one, an empty list throws. - Without a comparator,
sortcompares items as strings, so[450, 1220, 99]sorts to[1220, 450, 99]. - A comparator returns a negative number to put
afirst, positive to putbfirst;(a, b) => a - bsorts numbers ascending. sortmutates the array in place; usetoSorted(or sort a copy) when the original must stay as it is.