Hung-Yi's LogoHung-Yi’s Journal

JavaScript's Reduce: A Swiss Army Knife for Arrays

This is my favorite array and list manipulation tool in JavaScript. Avoid the mess of for/while loops and hard-to-debug variable assignments with reduce()!

There’s a little Array method in JavaScript that I’ve seen a lot of developers overlook. Admittedly, it’s abstract and generic in nature so doesn’t lend itself to any obvious uses, but it turns out that’s exactly what makes a great programming Swiss Army knife: you can do a lot more with it than you might realise.1

Today, let me introduce you to Array.prototype.reduce(). I’ll show you some creative ways to use it that might just make you like JavaScript after all!

The Basics

There are 2 key ideas to understanding reduce():

  1. It loops through every item in an array, one by one; and
  2. It accumulates results (by calling a function that you define) and spits it out at the end as the return value.

To remind myself of these 2 core concepts, I usually use a lambda with the input parameters as acc (a.k.a. accumulated results) and curr (a.k.a. current item of the array that we’re looping through). The names of the local variables help set the context in my head that I’m writing a reducer function. That said, feel free to name them however you please.

The Classic Example

Summing a list of numbers is an example that’s been repeated ad nauseam2 on every page of documentation ever written on fold/reduce/accumulate/aggregate/compress/inject:

// Calculate the sum of a list of numbers
[1,2,3,4].reduce((acc, curr) => acc + curr, 0); // => 10

Admittedly, it’s a great use case for reduce. But because this example is so common, I’m sure many people think numerical summation or aggregation is the only strong suit of reduce; I assure you it’s not!

First let’s take a detour to give you a taste of how fundamental the concept of reduce actually is.

More Primitive Than Map, Filter, Includes & Find

You may be familiar with array functions such as map, filter, includes and find already. These are the bread-and-butter for list processing in JavaScript. I use them all the time, of course.

But did you know that you can implement them using reduce? That’s right. Because reduce is a more primitive and flexible building block for list processing, it can do everything that the friendlier functions can.

Implementing map using reduce

const input = [1.618, 2.718, 3.141592653, 2.5029];

return {
  // Use map() to round a list of real numbers to the nearest integer
  roundedMap:
    input.map(x => Math.round(x)),

  // Do the same using reduce()
  roundedReduce:
    input.reduce((acc, curr) => [...acc, Math.round(curr)], [])
};
{ roundedMap: [ 2, 3, 3, 3 ], roundedReduce: [ 2, 3, 3, 3 ] }

Implementing filter using reduce

const input = [1.618, 2.718, 3.141592653, 2.5029];

return {
  // Use filter() to find all numbers above 2
  above2Filtered:
    input.filter(x => x > 2),

  // Do the same using reduce()
  above2Reduced:
    input.reduce((acc, curr) => curr > 2 ? [...acc, curr] : acc, [])
};
{
  above2Filtered: [ 2.718, 3.141592653, 2.5029 ],
  above2Reduced: [ 2.718, 3.141592653, 2.5029 ]
}

Implementing includes using reduce

const input = [1.618, 2.718, 3.141592653, 2.5029];
const pi = 3.141592653;

return {
  // Use includes() to see if the collection contains pi
  containsPiIncludes:
    input.includes(pi),

  // Do the same using reduce()
  containsPiReduced:
    input.reduce((acc, curr) => acc || curr === pi, false)
};
{ containsPiIncludes: true, containsPiReduced: true }

Implementing find using reduce

const input = [1.618, 2.718, 3.141592653, 2.5029];

return {
  // Use find() to get the first item that is larger than 2
  firstAbove2Find:
    input.find(x => x > 2),

  // Do the same using reduce()
  firstAbove2Reduced:
    input.reduce((acc, curr) => acc || (curr > 2 ? curr : undefined), undefined)
};
{ firstAbove2Find: 2.718, firstAbove2Reduced: 2.718 }

Note that the above implementations are for demonstration purposes only. They’re not as optimized and definitely less readable than their simpler counterparts. Please do not blindly replace everything with reduce.

However, if customizing the use of reduce above allows you to achieve something that you couldn’t before, then by all means reduce away! Which brings us to…

Some Realistic Use Cases

I’ll show you a few creative uses for reduce that you might not have thought of, but this is by no means an exhaustive list. reduce is too flexible of a tool to have a finite list of use cases.

As long as you have to solve a problem with a list or array, you should consider reduce if there’s no other readily-available way to do it.

Example 1: Key-value Pair Aggregation

Sometimes you’ll find yourself with an array of key-value pairs that you would rather have as one single JavaScript object instead. There are several ways to do this3, but reduce can be a really good, idiomatic choice.

const pairs = [
  { key: 'apple',  value: 5  },
  { key: 'orange', value: 3  },
  { key: 'banana', value: 10 }
];

return pairs.reduce((acc, curr) => ({...acc, [curr.key]: curr.value}), {});
{ apple: 5, orange: 3, banana: 10 }

Example 2: Group By

In plain JavaScript there’s no obvious way to do group-by on a key. Your options are:

  1. Use an external library like Lodash
  2. Loop through manually and build your own map or JavaScript object
  3. Use reduce

I think the most idiomatic way is to use reduce since it avoids side effects and doesn’t leave behind any garbage assignments to clean up. With reduce you also don’t have to rely on third party libraries that might add bloat to your code too.

This is how:

const ingredients = [
  { name: 'celery',     category: 'fiber'   },
  { name: 'potato',     category: 'carb'    },
  { name: 'egg',        category: 'protein' },
  { name: 'flour',      category: 'carb'    },
  { name: 'butter',     category: 'fat'     },
  { name: 'spinach',    category: 'fiber'   },
  { name: 'bread',      category: 'carb'    },
  { name: 'mayonnaise', category: 'fat'     },
  { name: 'chicken',    category: 'protein' },
];
// Let's group by the category into a JavaScript Map using reduce()
return ingredients.reduce((acc, curr) => {
  const existingGroup = acc.get(curr.category);
  return acc.set(
    curr.category,
    existingGroup ? [...existingGroup, curr] : [curr]
  ); // Map.set() returns the Map itself for convenience
}, new Map());
Map(4) {
  'fiber' => [
    { name: 'celery', category: 'fiber' },
    { name: 'spinach', category: 'fiber' }
  ],
  'carb' => [
    { name: 'potato', category: 'carb' },
    { name: 'flour', category: 'carb' },
    { name: 'bread', category: 'carb' }
  ],
  'protein' => [
    { name: 'egg', category: 'protein' },
    { name: 'chicken', category: 'protein' }
  ],
  'fat' => [
    { name: 'butter', category: 'fat' },
    { name: 'mayonnaise', category: 'fat' }
  ]
}

Example 3: Windowing and Partitioning

Actually, reduce is not not limited to mapping one-to-one or reducing to a single return value. Just like how filter can return fewer items than the original array, we can extend this concept to do all sorts of interesting structural transformations.

For example, let’s take this long list of people and group them into pairs:

const people = [
  'Alice', 'Bob',    'Charlie', 'Daisy', 'Edna',
  'Fara',  'Gordon', 'Hubert',  'Iris',  'Julian'
];
const groupsOf = 2;
return people.reduce((acc, curr, i) => {
  if (i % groupsOf === 0) {
    acc.push([curr]);
  } else {
    acc[acc.length - 1].push(curr);
  }
  return acc;
}, []);
[
  [ 'Alice', 'Bob' ],
  [ 'Charlie', 'Daisy' ],
  [ 'Edna', 'Fara' ],
  [ 'Gordon', 'Hubert' ],
  [ 'Iris', 'Julian' ]
]

Challenge Example: Redux-style Actions and Reducers

I’m pretty sure the idea of redux reducers came from the general concept of functional reduction. In fact, it would be correct to call the lambda function inside the reduce examples above as “reducers”, since that function’s job is to do the reducing.

When you reduce over some actions, you’re essentially looping over them and accumulating their effects on some application state. Here’s a toy calculator that follows this pattern.

const actionTypes = {
  ADD:      0,
  SUBTRACT: 1,
  DIVIDE:   2,
  MULTIPLY: 3
};
const actions =  [
  { type: actionTypes.ADD,      payload: 5  },
  { type: actionTypes.SUBTRACT, payload: 1  },
  { type: actionTypes.MULTIPLY, payload: 8  },
  { type: actionTypes.MULTIPLY, payload: 32 },
  { type: actionTypes.SUBTRACT, payload: 24 },
  { type: actionTypes.DIVIDE,   payload: 5  },
  { type: actionTypes.ADD,      payload: 2  },
  { type: actionTypes.MULTIPLY, payload: 10 },
];
const initialState = [0];
return actions.reduce(
  (state, action, i) => {
    switch (action.type) {
      case actionTypes.ADD:
        return [...state, state[i] + action.payload];
      case actionTypes.SUBTRACT:
        return [...state, state[i] - action.payload];
      case actionTypes.DIVIDE:
        return [...state, state[i] / action.payload];
      case actionTypes.MULTIPLY:
        return [...state, state[i] * action.payload];
    }
  },
  initialState
);
[
     0,    5,   4,  32,
  1024, 1000, 200, 202,
  2020
]

A New Tool for Your Tool-belt

I hope I’ve shown that reduce is worth the effort to consider when dealing with lists and arrays, especially when none of the existing array functions do exactly what you want.

It’s a primitive tool that can be a little mind-bending at first—but if you give it a little time, it can pay you back with its flexibility and elegance, allowing you to use one generic software pattern to solve many different types of problems.

Footnotes:

1

And just like a Swiss Army knife, just because you can do almost everything with it doesn’t mean you should. Don’t try to fell a tree with a Swiss Army knife; and don’t try to use reduce to sort an array.

2

A poor transistor dies a painful death each time a summation example is used to explain reduce().

3

If your data is in the right format, you can use Object.fromEntries(). If it isn’t in the right format, you can map it into the right format beforehand, but by then you might as well use reduce and get it done in one step. You might also consider new Map(pairs) if you’re using the object as a dictionary-like lookup.