Skip Main Navigation
Ben IlegboduBen Ilegbodu

Learn the Array reduce method by re-implementing lodash functions

Re-implement zip, keyBy, sum and other lodash functions in order to learn how Array reduce works

Sunday, July 18, 2021 · 6 min read

The built-in JavaScript Array object has lots of helpful methods that allow us to manipulate arrays. I use .map() and .filter() all the time when I'm transforming data. Arguably the most powerful, yet also least understood method is .reduce(). It allows us to basically transform an array into nearly anything else: another array, an object, a boolean, a number, etc.

So let's learn how .reduce() works by re-implementing lodash functions.


sum()

The _.sum() function computes the sum of the values in an array.

const sum = (array) => {
  return array.reduce((count, value) => {
    return count + value
  }, 0)
}

sum([1, 7, 3, -4, 5]) // 12

The sum() function is probably the most common example used when explaining .reduce(). It transforms an array of numbers into a single number. Let's quickly look at how .reduce().

The .reduce() method takes a function (called a "reducer") and an initial value. The initial value is technically optional, but we won't worry about that here. The reducer function is called on each element of the array. It takes two parameters, the value we're building up (called the "accumulator") and the current array element in the iteration. It also takes the index as the 3rd parameter and the entire array as the 4th, but we won't worry about that here either.

So in the example, our initial value is 0. That becomes count in the first iteration. We add 0 (i.e. count) to 1 (i.e. value) and return that value. In the next iteration, count is now 1 and value is 7. We add those together and return the value. So on the third iteration, count is now 8. The iterations continue until the final result returned by .reduce() is 12, which is then returned by our sum() function.

You may be asking, why not use a for loop or .forEach() for something so simple? One advantage of .reduce() is that it's chainable with other array methods.

const games = [
  { us: 109, them: 89 },
  { us: 78, them: 100 },
  { us: 94, them: 84 },
]
const ourPoints = games
  .map((game) => game.us)
  .reduce((totalPoints, points) => totalPoints + points, 0)
// 281

Splitting up .map() and .reduce() allows each to have a single purpose. The .map() converts the array of game objects into an array of numeric scores. Then the .reduce() focuses on summing up those scores. For more complicated transformations, making this split usually makes the logic more maintainable.

And now that we know how .reduce() works, lets look at other implementations to drive home our understanding.


countBy

The _.countBy() function creates an object with keys that are the unique elements in the array, and values that are the number of times the key was in the array.

const countBy = (array) => {
  return array.reduce((obj, item) => {
    if (item in obj) {
      // `item` is already a key so increment
      obj[item] += 1
    } else {
      // first time seeing `item` so initialize it
      // w/ count of 1
      obj[item] = 1
    }

    // always return the `obj`
    return obj
  }, {})
}

countBy([
  'basketball',
  'football',
  'tennis',
  'basketball',
  'bowling',
  'tennis',
  'swimming',
  'basketball',
])
/*
{
  basketball: 3,
  football: 1,
  tennis: 2,
  bowling: 1,
  swimming: 1
}
*/

Here is an example of turning an array into an object. We start with an empty object. Then in the reducer function we check if the item is already in the obj. If it is, then we increment its count. If not, we add it to the obj with an initial count of 1. We must return obj from the reducer function so that it's available as obj in the next iteration.


flatten

The _.flatten function (also available as .flat() on arrays since ES2019) flattens the array a single level deep.

const flatten = (array) => {
  return array.reduce((flattened, value) => {
    // `.concat()` flattens the value if it's an array
    // when concatenating to return a new array
    return flattened.concat(value)
  }, [])
}

flatten([1, 2, [3, 4], 5, [6, 7]])
// [1, 2, 3, 4, 5, 6, 7]

Here we go from an array to a new (and flattened) array. The .concat() method concatenates two (or more) arrays together, returning a new array. So if value is an array, concatenating it to the flattened array will result in each element of value being added to the end of flattened as a new array. And if value is not an array, the single value is added to the end of the newly created array.


filter

The ._filter function iterates over the array, returning a new array containing elements that pass the test implemented by the provided function (called a "predicate").

const filter = (array, predicate) => {
  return array.reduce((filteredArray, value) => {
    // if `value` passes the `predicate` test
    // include it in the `filteredArray`
    if (predicate(value)) {
      filteredArray.push(value)
    }

    // always return the `filteredArray`
    return filteredArray
  }, [])
}

filter(
  [
    { team: 'PHX', titles: 0 },
    { team: 'HOU', titles: 2 },
    { team: 'UTA', titles: 0 },
    { team: 'DEN', titles: 0 },
    { team: 'LAL', titles: 17 },
  ],
  (teamInfo) => teamInfo.titles > 0,
)
/*
[
  { team: 'HOU', titles: 2 },
  { team: 'LAL', titles: 17 }
]
*/

Now, I would never use .reduce() to implement filter() since .filter() already exists on the Array object, but this shows how flexible .reduce() is. It can even implement one of the main Array utility methods!


fromPairs

The _.fromPairs function returns an object composed from key-value pairs. This is equivalent to Object.fromEntries() (ES2019).

const fromPairs = (array) => {
  return array.reduce((obj, value) => {
    // `value[0]` becomes the `obj` key
    // `value[1]` becomes the `obj` value
    obj[value[0]] = value[1]

    // always return `obj`
    return obj
  }, {})
}

fromPairs([
  ['sun', 'Sunday'],
  ['mon', 'Monday'],
  ['tue', 'Tuesday'],
  // ...
])
/*
{
  sun: 'Sunday',
  mon: 'Monday',
  tue: 'Tuesday',
  // ...
}
*/

keyBy

The _.keyBy() function creates an object lookup using the specified property name as the lookup key.

const keyBy = (array, propName) => {
  return array.reduce((lookup, value) => {
    lookup[value[propName]] = value

    return lookup
  }, {})
}

keyBy(
  [
    { team: 'PHX', titles: 0 },
    { team: 'HOU', titles: 2 },
    { team: 'UTA', titles: 0 },
    { team: 'DEN', titles: 0 },
    { team: 'LAL', titles: 17 },
  ],
  'team',
)
/*
{
  PHX: { team: 'PHX', titles: 0 },
  HOU: { team: 'HOU', titles: 2 },
  UTA: { team: 'UTA', titles: 0 },
  DEN: { team: 'DEN', titles: 0 },
  LAL: { team: 'LAL', titles: 17 },
}
*/

map

The _.map() function, which of course exists as `.map() on arrays, creates a new array of values populated with the results of calling the provided function on every element of the array.

const map = (array, mapFn) => {
  return array.reduce((mappedArray, value) => {
    mappedArray.push(mapFn(value))

    return mappedArray
  }, [])
}

map([2, 5, 9], (n) => n ** 2)
// [4, 25, 81]

By the way, the map function in the example above is using the exponentiation (**) operator which was introduced in ES2016. It's equivalent to Math.pow().


max

The _.max() function computes the maximum value within the array.

const max = (array) => {
  return array.reduce((maxInProgress, value) => {
    return Math.max(maxInProgress, value)
  }, -Infinity)
}

max([4, 582, 38, -472, 1, 20])
// 582

We're using Math.max() to get the maximum value between the maxInProgress and the next value in the array. But Math.max() accepts more than 2 arguments, so we can implement our max() function with .reduce().

const max = (array) => Math.max(...array)

max([4, 582, 38, -472, 1, 20])
// 582

Using the spread operator allows us to convert the array of numbers into multiple arguments to Math.max().


some

The _.some() function tests whether at least one element in the array passes the test implemented in the function (called a "predicate"). This already exists as .some() on the Array object.

const some = (array, predicate) => {
  return array.reduce((passes, value) => {
    // if we haven't found a passing value yet,
    // try this next `value`
    if (!passes) {
      return predicate(value)
    }

    // otherwise we're good and return `true`
    return true
  }, false)
}

some([2, 12, 4, 8, 11], (n) => n > 10)
// true

While this shows reducing an array down to a boolean value, it is actually a suboptimal solution. The real implementation of .some() short-circuits. Once it has found a value that passes the test, it stops iterating through the array and returns true. There's no need to keep looking. Because .reduce() walks through the entire array, even when we've found a passing value, it keeps going. This would perform poorly for large arrays, but it shows off the functionality nonetheless.


zip

The _.zip() function creates an array of grouped elements, the first of which contains the first elements in the given arrays, the second which contains the second elements in the given arrays, etc.

const zip = (...arrays) => {
  return arrays[0].reduce((zipped, value, index) => {
    zipped.push(
      // generate an array of the element at the `index`
      // in each of the `arrays`
      arrays.map((array) => array[index]),
    )

    return zipped
  }, [])
}

zip(
  ['Houston', 'Miami', 'Chicago', 'Los Angeles'],
  ['Texas', 'Florida', 'Illinois', 'California'],
  ['713', '305', '312', '213'],
)
/*
[
  ['Houston', 'Texas', '713'],
  ['Miami', 'Florida', '305'],
  ['Chicago', 'Illinois', '312'],
  ['Los Angeles', 'California', '213]
]
*/

Here we get to use the third parameter of the reduce function, the value's index within the array.


So that's it. My hope is that seeing different implementations of functions, especially the familiar ones, will help you understand .reduce() just a little bit better. In my opinion, if you're able to add .reduce() to your Javascript development skill set, it will improve your data transformation code.

If you've got any thoughts or questions, feel free to reach out to me on Twitter at @benmvp.

Keep learning my friends. 🤓

Subscribe to the Newsletter

Get notified about new blog posts, minishops & other goodies


Hi, I'm Ben Ilegbodu. 👋🏾

I'm a Christian, husband, and father of 3, with 15+ years of professional experience developing user interfaces for the Web. I'm a Google Developer Expert Frontend Architect at Stitch Fix, and frontend development teacher. I love helping developers level up their frontend skills.

Discuss on Twitter // Edit on GitHub