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()
:
- It loops through every item in an array, one by one; and
- 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:
- Use an external library like Lodash
- Loop through manually and build your own map or JavaScript object
- 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:
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.
A poor transistor dies a painful death each time a summation example is used to explain reduce()
.
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.