The title says it all π. I want to talk about my all-time favorite javascript array method: Array.reduce(). I know there are a lot of contenders out there, but hear me out. reduce() is not just a method; it's a way of lifeβ¨.
I'm not going to lie though, when I first started and discovered reduce, it was a little intimidating. It took me a while before I was confidently using it everywhere in my code. But when I did, it was a game changer. Suddenly, I could perform complex operations on arrays with ease, transforming them into whatever I needed. My code become faster and cleaner.
But don't just take my word for it. Let me show you some of the things you can achieve with reduce(). It's time to dive into Array.reduce() and discover why it's absolutely goated! π
9 Use Cases for Array.reduce() π
Use Case 1: Summing Numbers
One of the most straightforward use cases for reduce() is summing up a bunch of numbers. Let's say you have an array of integers and you want to find the total sum.
const numbers: number[] = [1, 2, 3, 4, 5];
const sum: number = numbers.reduce((acc, curr) => acc + curr, 0);
console.log(sum); // Output: 15
Boom! With just one line of code, you've calculated the sum of all elements in the array. The initial value of the accumulator is set to 0, and in each iteration, we add the current element to the accumulator.
** Bonus: If you choose to leave out the starting value, reduce will just use the first item in the array. However I tend to always include an initial value, so it's easier to read.
Use Case 2: Flattening Arrays
Have you ever found yourself with an array of arrays and thought, "I wish I could flatten this into a single array"?
const nestedArray: number[][] = [[1, 2], [3, 4], [5, 6]];
const flattenedArray: number[] = nestedArray.reduce((acc, curr) => acc.concat(curr), []);
console.log(flattenedArray); // Output: [1, 2, 3, 4, 5, 6]
In this example, we start with an empty array as the initial accumulator value. Then, in each iteration, we concatenate the current sub-array to the accumulator using the concat() method. By the end, we have a perfect flattened array.
I know that you can also do this with Array.flat()
. However, it's important to know how to use reduce, in case you want to perform extra operations on each item.
Use Case 3: Grouping Objects
Imagine you have an array of objects, and you want to group them based on a specific property. reduce() is the perfect tool for the job.
interface Person {
name: string;
age: number;
}
const people: Person[] = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 25 },
{ name: 'Dave', age: 30 }
];
const groupedByAge: { [key: number]: Person[] } = people.reduce((acc, curr) => {
if (!acc[curr.age]) {
acc[curr.age] = [];
}
acc[curr.age].push(curr);
return acc;
}, {});
console.log(groupedByAge);
/*
Output:
{
'25': [{ name: 'Alice', age: 25 }, { name: 'Charlie', age: 25 }],
'30': [{ name: 'Bob', age: 30 }, { name: 'Dave', age: 30 }]
}
*/
In this case, we use an object as the initial accumulator value. We check if the accumulator already has a property for the current age. If not, we create an empty array for that age. Then, we push the current object into the corresponding age array. By the end, we have an object where the keys are the ages, and the values are arrays of people with that age.
You can now also now the newer groupBy
method, however, this tried and true classic is important to understand.
Use Case 4: Creating Lookup Maps
My personal favorite is using reduce() to create lookup maps from arrays. It's a game-changer when it comes to performance and code readability. Stop using those slow find() or filter() calls.
interface Product {
id: number;
name: string;
price: number;
}
const products: Product[] = [
{ id: 1, name: 'Laptop', price: 999 },
{ id: 2, name: 'Phone', price: 699 },
{ id: 3, name: 'Tablet', price: 499 },
];
const productMap: { [key: number]: Product } = products.reduce((acc, curr) => {
acc[curr.id] = curr;
return acc;
}, {});
console.log(productMap);
/*
Output:
{
'1': { id: 1, name: 'Laptop', price: 999 },
'2': { id: 2, name: 'Phone', price: 699 },
'3': { id: 3, name: 'Tablet', price: 499 }
}
*/
// Accessing a product by ID
const laptop: Product = productMap[1];
console.log(laptop); // Output: { id: 1, name: 'Laptop', price: 999 }
By using reduce() to create a lookup map, you can access elements by their unique identifier in constant time complexity. No more looping through the array to find a specific item.
Use Case 5: Counting Occurrences
Ever needed to count the occurrences of elements in an array? reduce() has got you covered.
const fruits: string[] = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
const fruitCounts: { [key: string]: number } = fruits.reduce((acc, curr) => {
acc[curr] = (acc[curr] || 0) + 1;
return acc;
}, {});
console.log(fruitCounts);
/*
Output:
{
'apple': 3,
'banana': 2,
'orange': 1
}
*/
In this example, we initialize an empty object as the accumulator. For each fruit in the array, we check if it already exists as a property in the accumulator object. If it does, we increment its count by 1; otherwise, we initialize it to 1. The result is an object that tells us how many times each fruit appears in the array.
Use Case 6: Composing Functions
Functional programming enthusiasts will enjoy this one. reduce() is a powerful tool for composing functions. You can use it to create a pipeline of functions that transform data step by step.
const add5 = (x: number): number => x + 5;
const multiply3 = (x: number): number => x * 3;
const subtract2 = (x: number): number => x - 2;
const composedFunctions: ((x: number) => number)[] = [add5, multiply3, subtract2];
const result: number = composedFunctions.reduce((acc, curr) => curr(acc), 10);
console.log(result); // Output: 43
In this example, we have an array of functions that we want to apply in sequence to an initial value of 10. We use reduce() to iterate over the functions, passing the result of each function as the input to the next one. The final result is the outcome of applying all the functions in the composed order.
Use Case 7: Implementing a Simple Redux-like State Management
If you've worked with Redux, you know how powerful it is for managing state in applications. Guess what? You can use reduce() to implement a simple Redux-like state management system.
interface State {
count: number;
todos: string[];
}
interface Action {
type: string;
payload?: any;
}
const initialState: State = {
count: 0,
todos: [],
};
const actions: Action[] = [
{ type: 'INCREMENT_COUNT' },
{ type: 'ADD_TODO', payload: 'Learn Array.reduce()' },
{ type: 'INCREMENT_COUNT' },
{ type: 'ADD_TODO', payload: 'Master TypeScript' },
];
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'INCREMENT_COUNT':
return { ...state, count: state.count + 1 };
case 'ADD_TODO':
return { ...state, todos: [...state.todos, action.payload] };
default:
return state;
}
};
const finalState: State = actions.reduce(reducer, initialState);
console.log(finalState);
/*
Output:
{
count: 2,
todos: ['Learn Array.reduce()', 'Master TypeScript']
}
*/
In this example, we have an initial state object and an array of actions. We define a reducer function that takes the current state and an action, and returns a new state based on the action type. By using reduce(), we can apply each action to the state in sequence, resulting in the final state. It's like having a mini-Redux.
Use Case 8: Generating Unique Values
Sometimes, you might have an array with duplicate values, and you need to extract only the unique ones. reduce() can help you accomplish that with ease.
const numbers: number[] = [1, 2, 3, 2, 4, 3, 5, 1, 6];
const uniqueNumbers: number[] = numbers.reduce((acc, curr) => {
if (!acc.includes(curr)) {
acc.push(curr);
}
return acc;
}, []);
console.log(uniqueNumbers); // Output: [1, 2, 3, 4, 5, 6]
Here, we initialize an empty array as the accumulator. For each number in the original array, we check if it already exists in the accumulator using the includes() method. If it doesn't, we push it into the accumulator array. The final result is an array containing only the unique values from the original array.
Use Case 9: Calculating Average
Want to calculate the average of a set of numbers? reduce() has got your back!
const grades: number[] = [85, 90, 92, 88, 95];
const average: number = grades.reduce((acc, curr, index, array) => {
acc += curr;
if (index === array.length - 1) {
return acc / array.length;
}
return acc;
}, 0);
console.log(average); // Output: 90
In this example, we initialize the accumulator to 0. We iterate over each grade and add it to the accumulator. When we reach the last element (checked using the index and array.length), we divide the accumulator by the total number of grades to calculate the average.
Performance Considerations ποΈ
While Array.reduce() is incredibly powerful and versatile, it's important to be aware of potential performance drawbacks, especially when dealing with large arrays or complex operations. One common pitfall is creating new objects or arrays in each iteration of reduce(), which can lead to excessive memory allocation and impact performance.
For example, consider the following code:
const numbers: number[] = [1, 2, 3, 4, 5];
const doubledNumbers: number[] = numbers.reduce((acc, curr) => {
return [...acc, curr * 2];
}, []);
console.log(doubledNumbers); // Output: [2, 4, 6, 8, 10]
In this case, we're using the spread operator (...) to create a new array in each iteration, which can be inefficient. Instead, we can optimize the code by mutating the accumulator array directly:
const numbers: number[] = [1, 2, 3, 4, 5];
const doubledNumbers: number[] = numbers.reduce((acc, curr) => {
acc.push(curr * 2);
return acc;
}, []);
console.log(doubledNumbers); // Output: [2, 4, 6, 8, 10]
By mutating the accumulator array using push(), we avoid creating new arrays in each iteration, resulting in better performance.
Similarly, when working with objects, it's more efficient to mutate the accumulator object directly rather than creating a new object with the spread operator:
const people: Person[] = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 25 },
{ name: 'Dave', age: 30 }
];
const groupedByAge: { [key: number]: Person[] } = people.reduce((acc, curr) => {
if (!acc[curr.age]) {
acc[curr.age] = [];
}
acc[curr.age].push(curr);
return acc;
}, {});
By mutating the accumulator object directly, we optimize the performance of the reduce() operation.
However, it's worth noting that in some cases, creating new objects or arrays in each iteration may be necessary or more readable. It's important to strike a balance between performance and code clarity based on your specific use case and the size of the data you're working with.
Conclusion
There you have it. Nine incredible use cases that showcase the power and versatility of Array.reduce(). From summing numbers to flattening arrays, grouping objects to creating lookup maps, counting occurrences to composing functions, and even implementing state management and calculating averages, Array.reduce()
proves to be a powerful tool in your js toolkit.
What do you think? What is your favorite array method and why?
Thanks for reading, and may the power of reduce() be with you. β¨πβ¨
Oh and one last shameless plug π
If you work in an agile dev team, check out my free planning poker and retrospective app called Kollabe.
Top comments (70)
Counterpoint from Google's Jake Archibald:
Along with the follow-up YouTube video: Is reduce() bad? - HTTP 203
So can map, forEach, flat, sort, it's non-argument. Everything is readable to human and is a preference. Reduce is a functional construct that exist in all functional languages and is pulled into most imperative languages too. JS should add reduceLeft.
I'm with Jake. I'd only use Reduce to reduce a list of number to a single value.
Except reduce is way more readable if you use it along with functional style code, and think in the computations with a immutable way is easier, once you know both approachs.
I guess that traditional loops are only more readable if you prefer an imperative style of programming or really like procedural paradigm.
Reduce Scrooge
Reduce is cool but it's not readable or intuitive which slows down development with teams.
When I see someone at work use reduce, they're usually being clever. I don't like clever when there was a simple option.
I'd almost always prefer to see .filter, .map, or just a good old fashioned for/in or for/of loop with relative mutable variables or arrays/objects defined above it.
Reduce is great, but it gets a 0 for readability and a 0 for simplicity in my book.
Simple is boring atimes... If you name your functions well/add the write comment then it's not a problem.
The only argument you can bring against using reduce is performance...
Respectfully... I'm so glad we don't work together if you're argument is "simplicity is boring".
Also, reduce is never actually used in a readable way. Everyone wants to claim to use best practices but in the wild it's always
acc: any ..., {}
Or something like that.
So you've got poor performance, bad patterns encouraged, and it only takes five times longer to grok.
reduce actually is the solution when your array methods are slow, because chain map, filter, etc causes more cycles through the array, so you can do all at the same time with a reduce, as any array traversing operation can be expressed by a reduce.
Its called transducers, and a transducer is a really elegant way to express composed array operations.
If you like a imperative style of programming, and/or mainly the procedural paradigm, sure, reduce never will be the more readable solution, but if you program in a declarative way, composing computations, reduce will be more readable than any imperative loop, as you can forget the array, think only in a function that maps one state to the next state, and that will work in the reduce.
Nice article with great examples!
I especially appreciate the Performance Considerations section, as the creation of object on every loop adds up quickly. Comparing a mutating reducer and a new-object-per-loop reducer, by 1000 entries a "bad" reducer will run for almost ~70 ms, while the object mutation reducer is under 1 ms. With 2000 entries that balloons to ~300 ms while the mutation is still 1 ms!
Thank you!
I'm glad you appreciate that. I've definitely come across this mistake in production applications, where it made a huge difference in render speeds. ~70ms times a few re-renders can be massive.
reduce
was never intended to cause side-effects (i.e.pushing
something onto an array you're not certain is owned by you alone). Yet, mutating the accumulator is the only solution to the performance caveat.Interestingly, functional programming flavored C++ counters the same problem by using something called 'linear types': data types that support 'growing' into new versions of themselves.
I heard it first at meetingcpp.com/mcpp/slides/2018/li...
For flattening js has flat array method, for unique values set, for map, of course map. s So reduce is one of tools to write your code but not the only one
Yes, this is outlined in the article. It's just another tool that is useful to understand. There will be use cases where it makes more sense to use reduce, and it's important to be able to understand why someone might be using it in a code review.
The commentary suggests you would need .reduce(...) if you wanted to transform along the way, but more intuitive for that would be .flatMap()
Hi, thanks for the article!
I want to make a small addition to a case with unique values in an array.
The time complexity for the specified option will be O(n^2)
Because there are two nested array iterations here: .reduce and .include
Which will be very ineffective on large input data.
this can be done in O(n) time using Set:
This is true, but not if you need to deal with non-primitive data like literal objects for example.
Great article. Small and well done salesmanship without taking 40% of the article with WHY it's good. Great examples. I've got code where I brute-forced thru some of this ... looking to find the time to do it more elegantly w/reduce.
The redux example is mindblowing!
Gave me a whole new perspective on reducers.
I'm glad you appreciated that! I was back and forth on whether or not I should include it, since it's pretty bulky. I really wanted to tie it to redux and also
useReducer
in React. They become a lot simpler when you understand the core concept.If you ever build a old-school js game, like chess, and want the application history like redux provides, you can easily achieve like this.
@mattlewandowski93
despite Redux uses reducers, array.reduce doesn't implement the Redux store, more than there is no array.reduce in redux
dispatch
. Even combineReducers doesn't use array.reduce.But, array.reduce is a good candidate to build the reducing decorator that can handle the batch of actions:
Such approach allows to avoid multiple dispatching (i.e. rendering) and keep initial reducer simple.
The full example here: TS Playground
Thanks for writing this!
I've now realized I've been taking the reducer in redux & useReducer for granted.
It's really refreshing to understand the fundamentals of the abstraction that I use daily.
@marcelc63
Why do you think Redux reducers are named "reducers"? :)))
The concept of Redux is a reducing of actions flow to the current (or final) state.
Great article!
This snippet above could've been shortened to this BTW.
can be further shortened to :
Haha! The nullish coalescing assignment!
You should check out Perl, haha.
Most of these can be written with
for...of/forEach
, which will also reduce cognitive complexity it introduces.Also, the easy way to check if you should use
reduce
or regular for loop is to ask one question: will the reduce work the same with and without default value. If yes, then use reduce, otherwise for loop is better choice.E.g. from examples, this will only work if the default value is empty object
Well, any array operation that can be expressed by a forEach can be expressed by a reduce, because they abstract almost the same thing. Reduce, or fold, is the abstraction for stateful computations in a world where you can only do stateless computation, so if you ever need to express an forEach in a immutable way, you can do by using a reduce, and if you need to express a reduce in a mutable way you can use a for loop or forEach.
To be fair, at this point, you don't need any array method, as all of them could be expresed as some simple algorithm using for loops.
Its a matter if you prefer imperative or declarative style of programming, and if you really need to optimize for performance or a more elegant code is needed.
I agree with all what you said, as I have missed to point out why I the forEach/for is better in this case.
The examples in this post are not presenting or using reduce/fold as it was intended, with immutability in mind. For example, if we take into account the function from post
It introduces the mutation into the mix, therefore the usage of reduce here is redundant and we have not benefit from it, as we are just passing around the reference of an object. Imagine if we passed default accumulator value for reduce as an variable that we defined previously before using reduce, then effectively, both that variable and reduce's return would be the same object (pointer).
If we created a new object that would not mutate previous one, each time we iterate over the item, then that would be a proper use case of an reduce/fold.
Note: there are times where you will need to mutate an value because of performance, but in languages (like JS/TS in this example) where I have a option to use other methods that improve readability; I would rather go with the forEach/for than reduce/fold.
Donβt get me wrong, I like a good reduce function, but letβs not pretend that the stuff youβre showing off as examples are all easily doable with some of the other array methods, especially a forEach. The primary difference is that with a forEach you have to initialize your state (counter vars and such) outside of the function so that they persist through each iteration of the loop.
I donβt know that Iβd use example #8. Thatβs more easily done with a Set.
Hey @jdfwarrior_99, I am not pretending anything. I am simply showing use cases for the reduce method. There are always going to be many ways to approach any problem. All array methods are like forEach. They're just loops. Having the block scoped variable allows you to easily chain methods.
It can be done with Set, however, it could be beneficial to do it with reduce if you wanted to use the unique variable in the interaction. With Set, you would have to create the set first, then do another iteration of the data to use it. In applications where performance is critical, these things can make a big difference.
If we're talking about performance, using
for
loop always will be faster than any of the array methodsUnderstanding how to do that with reduce is important to knowing how to expand in the idea of unique. Consider a uniqBy function. You can just "use a Set" for that
@mattlewandowski93
Beware though, your approach in example 8 has a quadratic complexity, it will scale very badly performance wise. (It's a loop within a loop). Unless I'm sure I have no more than 10 elements in my array, I would definitely use a Set or a hash map (independently of using reduce or not)