Q1What is passed as the first argument to the callback you give reduce?
reduce — Folding an Array Into a Single Value
Learn reduce, which combines an array into one value. Covers the accumulator and initial value, sums, maximums, grouping, and the TypeError on empty arrays.
Say you need the total for an order's line items. map keeps the same number of elements and filter only removes some, so either way you get an array back. Neither one can combine an array into a single value like a total or a maximum.
This article covers reduce, which combines an array into one value, along with the initial value it starts from.
Combining an Array Into One Value — reduce and the Initial Value
Say you want to show a shopping cart's total on screen. With forEach, the variable holding the total has to live outside the loop, where any later line could overwrite it. And since its value changes with every addition, you have to declare it with let instead of const.
The array method reduce (a method that passes the elements to a callback one at a time and returns only the last return value) takes a callback whose first parameter is the accumulator (the parameter that holds the value returned by the previous call). Unlike with map and filter, the element comes second. The second argument to reduce itself is the initial value (the value the accumulator holds on the first call).
const cartItems = [
{ name: "Laptop Stand", price: 3200 },
{ name: "USB Hub", price: 2480 },
{ name: "HDMI Cable", price: 1180 },
];
// The first parameter is the previous call's return value; the second is the current element
const addPrice = (total, item) => total + item.price;
// The second argument, 0, becomes total on the first call
const totalPrice = cartItems.reduce(addPrice, 0);
console.log(totalPrice); // 6860
// What comes back is a single value, not an array
console.log(typeof totalPrice); // number
total on call 2. The 6860 returned by the last call is reduce's result.You don't need your own variable for the running total, because reduce takes care of passing the value from one call to the next. And what gets passed along doesn't have to be a number; it can be an element or an object, too.
Keeping the Larger One — Carrying a Number or an Element
Say you want to know which of your uploaded videos has the most views. This time you don't add anything up; you compare values and carry only the larger one into the next call. The question is what the accumulator should start as.
The accumulator can hold a number or the element itself. Pass 0 as the second argument and a number is carried from call to call; pass videos[0] and the element itself is carried. Make the initial value the same kind of value the callback returns.
const videos = [
{ title: "SQL Basics #1", views: 8200 },
{ title: "Linux Basics #3", views: 15400 },
{ title: "Git Fundamentals", views: 9700 },
];
// The initial value is a number, so it compares and returns numbers
const maxViews = videos.reduce((max, video) => Math.max(max, video.views), 0);
console.log(maxViews); // 15400
// The initial value is an element, so it compares and returns elements
const topVideo = videos.reduce(
(top, video) => (video.views > top.views ? video : top),
videos[0],
);
console.log(topVideo.title); // Linux Basics #3
Both calls to reduce compare views; the only difference is what gets passed along. If you gave the second one an initial value of 0, top.views would be undefined on the first call, so the comparison would be false every time and 0 would be returned all the way to the end.
An Initial Value of 0 Breaks With Negative Numbers
In an array that can contain negative values, like temperature readings, starting from 0 means 0 stays the larger value even when the real maximum is -3, so you get a result that isn't in the array. Compare the elements against each other, starting from the array's first element, and the range of the values no longer matters.
Folding Counts Into an Object — Grouping
Say you want to know how many support tickets there are in each category. Unlike a total, the result isn't a single number; you need one value per category. You could create an empty object outside the loop and add to it with forEach, but then the tally is once again left sitting outside the operation.
You can use {} as the initial value too. Here the accumulator is the object being tallied, and the callback updates a key and then returns that object. Reading a key that hasn't been added yet gives undefined, so add ?? 0 to start the count at 0.
const tickets = [
{ subject: "I'd like to return an item", category: "Returns" },
{ subject: "My order hasn't arrived", category: "Shipping" },
{ subject: "Please reissue my shipping label", category: "Shipping" },
];
// Add 1 to the count for each ticket's category
const countByCategory = tickets.reduce((counts, ticket) => {
counts[ticket.category] = (counts[ticket.category] ?? 0) + 1;
return counts; // Pass the updated counts to the next call
}, {}); // This {} becomes counts on the first call
console.log(JSON.stringify(countByCategory)); // {"Returns":1,"Shipping":2}
console.log(countByCategory["Shipping"]); // 2
counts it receives and returns that same object. The only object written to is the one passed as the initial value.The initial {} is a new object that holds only the tally, so nothing is written to the elements of tickets. The table below sums up the initial value and the callback's return value for the three uses covered so far.
| What you fold into | Initial value | What the callback returns |
|---|---|---|
| A total | 0 | The previous total plus the value |
| The largest element | The array's first element | Whichever element is larger |
| Counts per category | An empty object | The object with an updated key |
When There Are No Elements — reduce Without an Initial Value
A filtered result can end up empty. If you total the day's shipments on a day with no shipments at all, reduce either returns 0 or throws an error, depending on how you wrote it.
You can leave out the second argument, the initial value. If you do, the array's first element becomes the starting accumulator, and the first call to the callback gets the element at index 1. An empty array has no first element to take, so a TypeError is thrown.
const yesterdayAmounts = [3200, 2480, 1180];
const todayAmounts = [];
// Without an initial value, the first element, 3200, becomes the starting total
console.log(yesterdayAmounts.reduce((total, amount) => total + amount)); // 6860
// Even on an empty array, an initial value is returned as-is
console.log(todayAmounts.reduce((total, amount) => total + amount, 0)); // 0
// Without an initial value, there's no way to decide the starting total
// todayAmounts.reduce((total, amount) => total + amount);
// TypeError: Reduce of empty array with no initial value
This kind of bug is easy to miss when you write the code: it works with the data you have, and only breaks on the first day when nothing matches. When you pass a filtered result to reduce, pass an initial value in case it's empty.
Leave Out {} and the First Element Gets Written To
If you leave out the final {} in the countByCategory example, the ticket object tickets[0] itself becomes the starting counts. No error is thrown: a Shipping key gets added to that object, and the one Returns ticket is never counted. Even when the array isn't empty, always pass an initial value when folding into an object.
Knowledge Check
Answer each question one by one.
Q2If you pass an initial value of 0 to a reduce that compares the elements' views and returns the larger one, what comes back?
Q3What happens when you call reduce without an initial value on an empty array?