Reduce an array to a single value with vanilla JS

My friend Mark Buskbjerg and I are both members of the Vanilla JS Slack channel run by Chris Ferdinandi. Yesterday, Mark asked the channel for help with a challenging array structure. Let’s look at how I helped him come up with a solution using the really versatile Array.reduce() method!

The challenge

Mark had an array as follows. It’s an array of votes cast by users. The first value in each inner array is the ID of the item the user voted for, and the second value is a string to show whether it was an upvote or a downvote.

var votes = [
  ["1", "down"],
  ["2", "up"],
  ["1", "up"],
  ["2", "down"],
  ["3", "up"],
  ["2", "up"],
  ["3", "up"],
  ["2", "down"],
  ["1", "up"],
  ["1", "down"],
  ["2", "up"],
  ["1", "down"],
  ["2", "up"]
];

Mark wanted to sum up the upvotes and downvotes for each ID in the array. After a discussion, I helped him come up with a solution that does just that.

The solution

The first thing I did was create a function to pass into the Array.reduce() method. The function can accept up to four arguments, but I only needed the first two: the accumulator and the current value. The purpose of the current value is obvious; it’s the current item being processed in the array. As for the accumulator, the MDN Web Docs explain:

The accumulator accumulates the callback’s return values. It is the accumulated value previously returned in the last invocation of the callback.

Here’s the function with its parameters set up.

/**
 * Sum up the votes for each ID in the array of votes
 * @param  {Object} accumulator  The accumulated value from the last invocation
 * @param  {Array}  currentValue The current item being processed in the array
 * @return {Object}              The sum of votes for each ID
 */
var sumVotes = function (accumulator, currentValue) {
  // The function contents will go here
};

Let’s pass this into the Array.reduce() method now, because I need to explain one more thing for it to make sense.

In addition to the callback function, the method also accepts another argument. If supplied, it becomes the first argument to the first call of the callback function. In this case, I used an empty object literal ({}).

On the first invocation of the sumVotes() callback function, the accumulator will be equal to the empty object literal, while the currentValue will be equal to the first value in the array (["1", "down"]).

// Sum up the votes for each ID in the array of votes
var results = votes.reduce(sumVotes, {});

Note: if you’re wondering how we can just pass in the name of the function without using any parentheses, I wrote a post about that.

Now, let’s dig into the actual function contents.

Create a property for the ID

For each invocation of the function, we need to create a property for the current vote ID if it doesn’t already exist. To do that, we’ll use a simple if statement.

The vote ID is the first item in the current array, so we use currentValue[0] to reference it. If the accumulator object doesn’t have the vote ID as a property, we create it and set its value to an array. We set both values in this new array (upvotes and downvotes) to 0.

// Create a property for the vote ID if it doesn't exist
if (!accumulator.hasOwnProperty(currentValue[0])) {
  accumulator[currentValue[0]] = [0, 0];
}

Upvotes or downvotes?

Now we need to figure out whether we’re increasing the upvotes or the downvotes for this vote ID. We’ll create a variable called upOrDown.

If the second value in the current array is "up", we’ll set this variable to 0; if it’s "down", we’ll set it to 1. These numbers correspond to the indices in the array of upvotes and downvotes for the current vote ID. The first item in the array is the upvotes (0), while the second item is the downvotes (1).

// Whether to increase upvotes or downvotes
var upOrDown;
if (currentValue[1] === "up") {
  upOrDown = 0;
} else if (currentValue[1] === "down") {
  upOrDown = 1;
}

Increase and return

Finally, we need to increase the upvotes/downvotes and return the accumulated value.

// Increase upvotes/downvotes
accumulator[currentValue[0]][upOrDown] += 1;

// Return the accumulated value
return accumulator;

Wrapping up

Here’s the complete function:

/**
 * Sum up the votes for each ID in the array of votes
 * @param  {Object} accumulator  The accumulated value from the last invocation
 * @param  {Array}  currentValue The current item being processed in the array
 * @return {Object}              The sum of votes for each ID
 */
var sumVotes = function (accumulator, currentValue) {

  // Create a property for the vote ID if it doesn't exist
  if (!accumulator.hasOwnProperty(currentValue[0])) {
    accumulator[currentValue[0]] = [0, 0];
  }

  // Whether to increase upvotes or downvotes
  var upOrDown;
  if (currentValue[1] === "up") {
    upOrDown = 0;
  } else if (currentValue[1] === "down") {
    upOrDown = 1;
  }

  // Increase upvotes/downvotes
  accumulator[currentValue[0]][upOrDown] += 1;
  
  // Return the accumulated value
  return accumulator;

};

After running the function through the Array.reduce() method, the following is the final value that gets returned and saved to the results variable.

The ID of each vote is a property of the object. The value of each property is an array. The first value in each array is the sum of upvotes for that ID, while the second value is the sum of downvotes.

var results = {
  "1": [2, 3],
  "2": [4, 2],
  "3": [2, 0]
};

The Array.reduce() method can be a little confusing. For more on this, I highly recommend you check out the post Using Array.reduce() in vanilla JS by Chris Ferdinandi. It really helped me get to grips with how it works. In particular, I found the following snippet very helpful:

Most of the modern array methods return a new array. The Array.reduce() method is a bit more flexible. It can return anything.

Its purpose is to take an array and condense its content into a single value.

That value can be a number, a string, or even an object or new array. That’s the part that’s always tripped me up. I didn’t realize just how flexible it is!