EduFlow
JavaScript Fundamentals
0%

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.

  1. 1reduce: many values into one
  2. 2sort: putting things in order

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:

JavaScript
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.

reduce starts with the initial value 0. The first call returns 0 plus 450, which is 450; the second returns 450 plus 1220, which is 1670; the third returns 1670 plus 1350, which is 3020, the final result. 0 initial 0 + 450 Coffee 450 + 1220 Train 1670 + 1350 Lunch 3020 returns 450 returns 1670
reduce passes the accumulator from one call to the next

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:

JavaScript
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:

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:

JavaScript
const amounts = [450, 1220, 99];
console.log([...amounts].sort((a, b) => a - b));  // ascending
console.log([...amounts].sort((a, b) => b - a));  // descending

When 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:

JavaScript
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:

JavaScript
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

  • reduce walks a list carrying an accumulator; the callback returns the next accumulator, and the last one is the result.
  • Always pass reduce an initial value, such as 0 for totals or {} for grouping; without one, an empty list throws.
  • Without a comparator, sort compares items as strings, so [450, 1220, 99] sorts to [1220, 450, 99].
  • A comparator returns a negative number to put a first, positive to put b first; (a, b) => a - b sorts numbers ascending.
  • sort mutates the array in place; use toSorted (or sort a copy) when the original must stay as it is.

Further reading

Check your understanding

4 questions

Question 1 of 4

What does [450, 1220, 99].sort() return?

Question 2 of 4

Which comparator sorts expenses from the most expensive to the cheapest?

Question 3 of 4

[].reduce((sum, e) => sum + e.amount) is called on an empty list. What happens?

Question 4 of 4

You show expenses sorted by amount in one panel and in the order they were added in another. Both panels read the same expenses array. What goes wrong with expenses.sort(byAmount)?

Practice

Code playground

Totals and rankings

Fill in four functions for Pocket’s summary screen:

  • totalCents(expenses) returns the sum of every amount (and 0 for an empty list). Use reduce.
  • totalsByCategory(expenses) returns an object with one property per category holding that category’s total, such as { food: 1800, travel: 1220, fun: 99 }.
  • biggestFirst(expenses) returns a new array sorted by amount, largest first.
  • alphabetical(expenses) returns a new array sorted by label from A to Z, treating “coffee” and “Coffee” alike.

Neither sort may reorder the array it receives.

const expenses = [
  { label: "coffee", amount: 450, category: "food" },
  { label: "Train", amount: 1220, category: "travel" },
  { label: "Lunch", amount: 1350, category: "food" },
  { label: "Museum", amount: 99, category: "fun" },
];

// Sum of all amounts (0 for an empty list)
function totalCents(expenses) {
  return 0;
}

// An object like { food: 1800, travel: 1220, fun: 99 }
function totalsByCategory(expenses) {
  return {};
}

// A NEW array, most expensive first; do not reorder the original
function biggestFirst(expenses) {
  return expenses.sort();
}

// A NEW array sorted by label, A to Z, ignoring upper/lower case
function alphabetical(expenses) {
  return expenses;
}

console.log(totalCents(expenses), totalsByCategory(expenses));
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Console

(no output)

Checks

  • totalCents sums the amounts and returns 0 for an empty list
  • totalsByCategory totals each category
  • biggestFirst sorts by amount, largest first
  • alphabetical sorts by label, ignoring case
  • The original array keeps its order