The JavaScript Reduce Method

Until recently, the only thing I've ever successfully reduced was myself - to tears. Much like the world's billionaires have recently embarked on journeys to shoot their wealthy bottoms to outer space, so, too, have I embarked on an epic adventure to understand the reduce method. Would you like to accompany me on my journey to finally, once and for all, understand the notorious, the dreaded, the one-and-only reduce method? Great. Welcome on board Rocket JavaScript. πŸš€

What is the reduce method?

Javascript comes with a bunch of inbuilt array methods designed to make our lives easier. They provide out-of-the-box, frequently needed functionality for iterating through or manipulating arrays in specific ways. There's quite a few of them and whilst it's completely unnecessary to remember them all, it's a good idea to have some solid intuitions about what can be achieved using them.

According to MDN, the reduce() method executes a callback function (that you provide) on each element of the array, resulting in a single output value. Apart from the callback function, it can also take in an initial value.

//reducer is the callback function, initialValue is the optional second param
array.reduce(reducer [, initialValue])

The reducer function

The callback function takes four arguments, but the last two can frequently be omitted, depending on what we want to achieve. The function is then applied to each element in the array, eventually returning a single value.

  1. Accumulator - this accumulates the reducer function's return values
  2. Current Value - the current element being processed
  3. Current Index (optional) - index of the current element being processed
  4. Source Array (optional) - the array we are calling the reduce method on
function reducer(accumulator, currentValue, currentIndex, array){}

This can all sound very confusing, so let's break it down and examine the syntax.

Let's assume we want to write a function that adds up all items in an array and returns their sum. The initial array we want to sum is the following. Let's ignore the fact it clearly adds up to 10 and pretend our mathematical ability is low enough to require us to find a programmatic solution to what we perceive to be an impossible numeric challenge.

const arr = [1,2,3,4]

Now let's look at how to apply the reduce method.

//define the reducer function, provide it with its first 2 parameters
//returns the sum of the accumulator and currentValue
const calculateSum = (accumulator, currentValue) => accumulator + currentValue

//apply reducer function to array
arr.reduce(calculateSum)

Above, we've told the reducer function to return the sum of the accumulator and the current value being processed. This means that as the reducer iterates through the array, each new number will be added to an ever-increasing sum held in the accumulator. Still confusing? I agree. Let's add in some console.logs to understand how the process executes.

Explanation

Throughout the article, I will post images to show how the accumulator and currentValue of the callback function change. I will then explain the image using words, which might or might not be useful for you. If you're a visual learner, you might find the images on its own more useful and feel confused by the text. Feel free to skip the bits that are not useful to your particular learning style.

const calculateSum = (accumulator, currentValue) => {
    console.log('accumulator: ', accumulator);
    console.log('currentValue:', currentValue);
    return accumulator + currentValue;
  };

arr.reduce(calculateSum)
  1. On the first iteration, the accumulator is the first item of the array, 1. The currentValue, or the item being processed, is the following item, 2. As we apply the reducer function to 2, the reducer returns the sum of the accumulator, 1 and the currentValue, 2.
  2. The return value of the reducer, 3, becomes the new accumulator. The currentValue shifts to the next item in the array, which happens to also be 3. The function to add the accumulator to the currentValue is applied to the currentValue of 3, which makes 3 + 3 and results in 6.
  3. 6 therefore becomes the new accumulator. The next item in the array, the currentValue, is now 4. The reducer that adds up the accumulator and currentValue now gets applied to 4. 6 + 4 is 10, and because there are no more items in the array, this becomes the final return value.

Phew. Turns out, not only is this array method hard to understand, it's also hard to describe. If my words confused you, I encourage you to step through the image line by line in your own time.

Note: By the way, this is not a common real-world use case for the reduce method. If all we want to do is sum an array of numbers, we might as well just use a for loop or forEach. Nevertheless, using reduce in this way serves as a good illustration of how the method works. We will encounter a few such 'bad-use-but-good-explanation-cases' over the course of this article.

Initial Value

We can also tell our reduce method to initialise the accumulator at an arbitrary value of our own choosing, by passing in the optional parameter of initialValue.

arr.reduce(reducer, initialValue)

Let's recycle above example.

const arr = [1,2,3,4]

const calculateSum = (accumulator, currentValue) => {
    console.log('accumulator: ', accumulator);
    console.log('currentValue:', currentValue);
    return accumulator + currentValue;
  };

//here we tell the reduce method to initialise the accumulator at 10
arr.reduce(calculateSum, 10)

In the previous version of this example, the first accumulator was 1, which is the first value of the array. Here, we override this value by adding a second argument to the reduce method, the initialValue of 10. 10 now becomes our first accumulator, and the reducer is applied to the first item in the array.

Here's a summary of how passing in the optional initial value parameter affects the execution of the reduce method.

initialValue accumulator currentValue
not passed accumulator = array[0] currentValue = array[1]
passed accumulator = initialValue currentValue = array[0]

Setting the initial value something other than a number, (e.g. an empty array or object) allows us to do some neat stuff with our reducers. Let's walk through a couple of examples.

1. Counting using reduce

Let's say we're looking to write a function that takes in a string and returns an object with a letter count for the given string. If our string was "save the bees" our desired return value would be

{ s: 2, a: 1, v: 1, e: 4, " ": 2, t: 1, h: 1, b: 1 }

const string = "πŸš«πŸš«πŸš€πŸš€ less rockets, more bees pls"

const letterCountReducer = (acc, value) => {
  acc[value] ? ++acc[value] : (acc[value] = 1);
  return acc;
};

//the accumulator is initialised as an empty object
[...string].reduce(letterCountReducer, {})

Explanation

image showing the beginning of the execution order of above process

  1. Because we passed in the initial value of an empty object, the accumulator is initialised as an empty object.
  2. As we iterate over the array, we can check whether each letter exists as a key in the accumulator object. If it does, we increment it by 1, if it doesn't, we initialise it with a value of 1.
  3. We return the new accumulator that now accounts for the letter we just iterated over, and move on. Eventually we'll return an accumulator that contains an object with all the letters accounted for.

2. Flattening arrays using reduce

Let's assume we have an array of arrays. Three types of animals, dying to be together, separated by indomitable array walls.

//BOO! An unnatural habitat
const zoo = [
  ['πŸ‡', 'πŸ‡', 'πŸ‡'],
  ['🐷', '🐷', '🐷'],
  ['🐻', '🐻', '🐻'],
];

How do we break them free?

const flatten = (acc, animalArray) => acc.concat(animalArray);

zoo.reduce(flatten, []);
//returns ["πŸ‡", "πŸ‡", "πŸ‡", "🐷", "🐷", "🐷", "🐻", "🐻", "🐻"]
//YAY! A natural habitat!

Explanation:

  1. We provide an empty array as the accumulator.
  2. The reducer concatenates the first current value, here named animalArray, to the empty accumulator. We return this new array, now filled with 3 bunnies.
  3. This becomes the new accumulator, to which we now concatenate the next currentValue, or animalArray. The second item in the original array is an array of pigs. We return the new accumulator consisting of bunnies and pigs, and move on to the bears. The accumulator is now an array of bunnies and pigs. To this, we concatenate the current value - the bear array.

Note: While this example serves to illustrate the workings of the reduce method, in practice, I'd opt for the arr.flat() method which does exactly what it says on the tin.

3. Deduplicating arrays using reduce

Let's assume we have an array with duplicates and want to end up with an array of unique values instead.

//initial arr
const arrOfDupes = ["πŸš€", "πŸš€", "πŸš€", "🌍"];

//desired output
 ["πŸš€", "🌍"];

const dedupe = (acc, currentValue) => {
  if (!acc.includes(currentValue)) {
    acc.push(currentValue);
  }
  return acc;
};

const dedupedArr = arrOfDupes.reduce(dedupe, []);

Explanation

  1. We start with the initial value of an empty array, which becomes our first accumulator.
  2. As the reduce method iterates over the array, the callback function gets applied to each item in the array. It checks for the absence of the current value from the accumulator. If this is the case, the current value gets pushed into the accumulator.
  3. The accumulator gets returned, either unchanged or with an additional unique value.

Note: While this example serves to illustrate the inner workings of the reduce method, in practice, I'd opt for deduplicating an array of primitives by using Sets, which is a more performant approach.

dedupedArr = [...new Set(array)];

4. Grouping items using reduce

Let's assume we want to group an array of objects by property. We start with an array of objects and end up with an object that includes two arrays where the objects are grouped by a selected property.

//initial array of objects to be grouped
const climateBehaviours = [
  { description: "Recycle", greenPoints: 30 },
  { description: "Cycle everywhere", greenPoints: 40 },
  { description: "Commute to work via plane", greenPoints: -70 },
  { description: "Replace beef with veg", greenPoints: 50 },
  { description: "Build a rocket for space tourism", greenPoints: -500 },
];

//desired output: an object with two groups
{
  goodClimateBehaviours: [{}, {}, ...], // greenPoints >= 0
  badClimateBehaviours: [{}, {}, ...],  // greenPoints < 0
};

Let's code this up.

//reducer function
const groupBehaviour = (acc, currentObj) => {
  currentObj.greenPoints >= 0
    ? acc.goodClimateBehaviours.push(currentObj)
    : acc.badClimateBehaviours.push(currentObj);
  return acc;
};

//initial value 
const initialGrouping = {
  goodClimateBehaviours: [],
  badClimateBehaviours: [],
};

//applying the reduce method on the original array
const groupedBehaviours = climateBehaviours.reduce(groupBehaviour, initialGrouping);

In bad news for the Musks, Bezoses and Bransons of this world, this is what we end up with.

console.log(groupedBehaviours)

{
  goodClimateBehaviours: [
    { description: "Recycle", greenPoints: 30 },
    { description: "Cycle everywhere", greenPoints: 40 },
    { description: "Replace beef with veg", greenPoints: 50 },
  ],
  badClimateBehaviours: [
    { description: "Commute to work via plane", greenPoints: -70 },
    { description: "Build a rocket for space tourism", greenPoints: -500 },
  ],
};

Explanation

  1. The initial value is an object with two properties, goodClimateBehaviours and badClimateBehaviours. This is our first accumulator.
  2. The callback reducer function iterates over the array of objects. Each time, it checks whether the current object has greenPoints greater than 0. If so, it pushes the object into accumulator.goodClimateBehaviours, otherwise the object is pushed to accumulator.badClimateBehaviours. The accumulator is then returned.
  3. An accumulator eventually containing all objects will be returned as the final return value.

5. Manipulating more complex data structures using reduce

In the real world, the power of reduce gets harnessed most commonly when manipulating more complex data structures. Let's say we have an array of objects with an id, description and outcomes array, where each outcome can be desirable or not. We want to transform this array into a single object that looks quite different.

const climateActions = [
  {
    id: 'space_tourism',
    description: 'build rockets for space tourism',
    outcomes: [
      { outcome: 'rich people can go to space', isDesirable: false },
      { outcome: 'is pretty cool', isDesirable: true },
      { outcome: 'increased emissions', isDesirable: false },
      {
        outcome: 'investment diverted from green energy to space tourism',
        isDesirable: false,
      },
    ],
  },
  {
    id: 'trees_4_lyf',
    description: 'stop burning down the amazon',
    outcomes: [
      { outcome: 'air for all', isDesirable: true },
      { outcome: 'our kids might live', isDesirable: true },
      {
        outcome: 'reduce threat of imminent extinction',
        isDesirable: true,
      },
      {
        outcome: 'make greta happy',
        isDesirable: true,
      },
      {
        outcome: 'make bolsonaro sad',
        isDesirable: false,
      },
    ],
  },
];

Our aim is to transform this array into a single object which has the id as keys, and an object with arrays of good outcomes and bad outcomes, as shown below.

const climateInitiatives = {
  'space_tourism': {
    badOutcomes: [
      'rich people can go to space',
      'increased emissions',
      'investment diverted from green energy to space tourism',
    ],
    goodOutcomes: ['is pretty cool'],
  },
  'trees_4_lyf': {
    badOutcomes: ['make bolsonaro sad'],
    goodOutcomes: [
      'air for all',
      'our kids might live',
      'reduce threat of imminent extinction',
      'make greta happy',
    ],
  },
};

Here's one way to implement this transformation, using reduce.

const reducer = (acc, currentObj) => {
  const newAcc = {
    ...acc,
    [currentObj.id]: { badOutcomes: [], goodOutcomes: [] },
  };

  currentObj.outcomes.map(outcome => {
    outcome.isDesirable
      ? newAcc[currentObj.id].goodOutcomes.push(outcome.outcome)
      : newAcc[currentObj.id].badOutcomes.push(outcome.outcome);
  });

  return newAcc;
};

const res = climateActions.reduce(reducer, {});

We could also, instead of using the map method, use a reduce within the reduce, but we might break the matrix in doing so. 🀯

Explanation

  1. The first accumulator is an empty object. The current value, named current object here, is the first object in the original array.
  2. The reducer function initialises a new variable, newAcc. newAcc is an object, with the spread of the current (still empty) accumulator. We assign a new property to newAcc, with the key being the current object's id, and the value being an object with the bad and good outcomes arrays. [currentObj.id]: { badOutcomes: [], goodOutcomes: [] }
  3. We then map over the current object's outcomes array and depending on whether the outcome is desirable, push it into or new variable newAcc's outcomes array.
  4. We return the newAcc, which becomes the acc on the next round of iteration, so when we spread it, we don't lose its contents.

Conclusion

What have we learned? Hopefully, the reduce method (and also that I am clearly not a huge fan of billionaires squandering resources on the selfish pursuit of space travel at a time when we should all focus on preventing catastrophic global heating, but that's just my opinion πŸ”₯).

Reduce is undoubtedly one of JavaScript's more tricky inbuilt methods. But as is the case with most coding, the best way to truly understand it is to practice. If the examples in this article made sense - great! If not - also great, one more opportunity to play around and practice until things click. And I promise, they eventually will.

Now, let's go and reduce some code. Also our emissions. πŸƒ

22