JavaScript ES6 Map and Reduce Patterns

Aron Racho · Aug 29, 2017

Javascript · Map · Reduce · Patterns · ES6

Over the past couple of months I've started to become a little bit more serious in applying functional programming techniques to my favorite client side language, JavaScript. In particular I've discovered the joy of using Map and Reduce.

These functions (in conjunction with other classic favorites such as .filter, .sort etc) and ES6 have made my code more expressive and easier to understand. I'd like to share some of the common map and reduce patterns I've come to use frequently in this blogpost, contrasting these techniques with the corresponding traditional imperative approach.

What are Map / Reduce Good For

Generally speaking, I turn to the map function if I need to transform some Array of one object type into another Array of a different object type. Likewise, I use the reduce function if I find it necessary to take an array of objects and boil it down to a non-array structure (either a primitive or some JSON object). A large portion of the rest of this blogpost are examples of variations on these two themes.

Why I Prefer Map / Reduce to For Loops

Its possible to achieve the transformations I mentioned above with traditional imperative techniques, largely involving for loops and mutable data structures.

However there are some advantages to using Map / Reduce over these imperative techniques:

  • Less code to wade through and maintain
  • Elimination of the use of mutable data structures and variables which leave room for unintended side effects
  • Encourages cleaner separation of concerns

I am hoping these benefits will become evident as I explore the map and reduce patterns below.

Summation (reduce)

The simplest application of reduce I can think of is summing up an array of numbers:

const total = arrayOfInt.reduce((ac, next) => ac + next, 0)

Contrast this against a traditional for loop approach:

let total
for (let i = 0; i < arrayOfInt.length; i++) {
  total += arrayOfInt[i]
}

By using reduce the code is less verbose, and we avoid having to declare a mutable total variable.

Indexing an array of objects (reduce)

Another common usage of reduce is to turn an array of objects into a single object indexed by some property of the object. In this case, we may have a simple array like this:

[
  {
    "id": "aron",
    "hobby": "games"
  },
  {
    "id": "scott",
    "hobby": "ninjitsu"
  }
]

... and we wish to represent this array as an object that looks like this:

{
  "aron": {
    "id": "aron",
    "hobby": "games"
  },
  "scott": {
    "id": "scott",
    "hobby": "ninjitsu"
  }
}

Here is how to do it via .reduce, the spread operator, and computed properties:

const indexedPeople = peopleArray.reduce((ac, p) => ({ ...ac, [p.id]: p }), {})

This powerful little statement is doing the following:

  1. For each person p in peopleArray find their id
  2. Make a copy of the ac map where there is a value whose key is the id from (1) and value is the person
  3. When iterating through the array is complete, set the latest incarnation of ac map to the invariant indexedPeople variable

Contrast the above code with this more traditional approach:

const indexedPeople = {}
for (let i = 0; i < peopleArray.length; i++) {
  const p = peopleArray[i]
  indexedPeople[p.id] = p
}

The functional approach using reduce accomplishes the goal with a single statement, which leaves no room for accidentally mutating state. This is not the case in the traditional approach, where i and indexedPeople could be inappropriately manipulated in some way to inadvertently introduce bugs, once more code (and complexity) is introduced inside of the loop.

Counting occurrences of items in an array (reduce)

Another useful application of the same ingredients (reduce, spread operator, and computed properties) is to create an object which counts the frequency of items within an array based on some key.

Given this input array:

[
  {
    "age": 33,
    "hobby": "games"
  },
  {
    "age": 28,
    "hobby": "chess"
  },
  {
    "age": 21,
    "hobby": "ninjitsu"
  },
  {
    "age": 21,
    "hobby": "games"
  },
  {
    "age": 21,
    "hobby": "kick the can"
  }
]

... we can use reduce to produce an object which lists the frequency of each hobby:

{
  "games": 2,
  "chess": 1,
  "ninjitsu": 1,
  "kick the can": 1
}

This can be accomplished as follows:

const accumulatedTotals = peopleArray.reduce(
  (totals, p) => ({ ...totals, [p.hobby]: (totals[p.hobby] || 0) + 1 }),
  {},
)

... which achieves it in the following manner:

  1. For each person p in peopleArray, and find their hobby
  2. Find the current total in the totals map for that hobby
  3. Create a new copy of the totals map where the count hobby's current total is incremented by 1
  4. When iterating through the array is complete, set the latest incarnation of totals map to the invariant accumulatedTotals variable

Like the previous indexing example, the functional approach seems less verbose and error prone than the traditional approach:

const accumulatedTotals = {}
for (let i = 0; i < peopleArray.length; i++) {
  const p = peopleArray[i]
  accumulatedTotals[p.hobby] = (accumulatedTotals[p.hobby] || 0) + 1
}

Flattening an object (map)

Until ES7 comes out with Object.values(...), a common way of converting an object's values into an array of values is to do so with map:

const peopleArray = Object.keys(indexedPeople).map((k) => indexedPeople[k])

Here we are creating a new array of people objects based on the keys of the indexedPeople map.

Here is the verbose imperative code for contrast:

const peopleArray = []
const keys = Object.keys(indexedPeople)
for ( let i = 0; i < keys.length; i++) {
	peopleArray.push( indexedPeople[ keys[i] ) )
}

Operation chaining encourages separation of concerns

In the following example, we are trying to convert an indexedPeople object into an array of people sorted by their age, excluding those under 21 years old.

Here's the map way of doing it

const peopleArray = Object.keys(indexedPeople)
  .map((k) => indexedPeople[k])
  .sort((a, b) => a.age - b.age)
  .filter((person) => person.age >= 21)

Contrasted with the traditional approach:

const peopleArray = []
const keys = Object.keys(indexedPeople)
for ( let i = 0; i < keys.length; i++) {
	const person = indexedPeople[ keys[i]
	if (person.age >= 21 ) {
		peopleArray.push(person) )
	}
}

// sort algorithm
const sort(a) => { ... does sorting ... }
sort(peopleArray)

An important thing to note is that the imperative example mixes the flattening and filtering operations in the same 'stage' of the computation. In contrast, the functional paradigm using map has more of a 'pipeline' approach, where the flattening, sorting, and filtering operations are more clearly discerned as separate steps.

This functional "pipeline" approach makes some operations easier to compose from smaller parts, which are easier to read, test, and maintain. The imperative approach on the other hand has fewer guard rails against bloating the individual pieces, and may lead some developers to make one piece do too much (as illustrated above).

Final thoughts

These aren't by any stretch of the imagination the only applications of map / reduce, but I hope that it is a helpful starting point for developing your own usage patterns.

Interested in working with us?