Multipurpose higher-order functions like reduce()
can be powerful but sometimes difficult to understand, especially for less-experienced JavaScript developers. If code becomes clearer when using other array methods, developers must weigh the readability tradeoff against the other benefits of using reduce()
.
Note that reduce()
is always equivalent to a for...of
loop, except that instead of mutating a variable in the upper scope, we now return the new value for each iteration:
const val = array.reduce((acc, cur) => update(acc, cur), initialValue);
let val = initialValue;
for (const cur of array) {
val = update(val, cur);
}
As previously stated, the reason why people may want to use reduce()
is to mimic functional programming practices of immutable data. Therefore, developers who uphold the immutability of the accumulator often copy the entire accumulator for each iteration, like this:
const names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];
const countedNames = names.reduce((allNames, name) => {
const currCount = Object.hasOwn(allNames, name) ? allNames[name] : 0;
return {
...allNames,
[name]: currCount + 1,
};
}, {});
This code is ill-performing, because each iteration has to copy the entire allNames
object, which could be big, depending how many unique names there are. This code has worst-case O(N^2)
performance, where N
is the length of names
.
A better alternative is to mutate the allNames
object on each iteration. However, if allNames
gets mutated anyway, you may want to convert the reduce()
to a simple for
loop instead, which is much clearer:
const names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];
const countedNames = names.reduce((allNames, name) => {
const currCount = allNames[name] ?? 0;
allNames[name] = currCount + 1;
return allNames;
}, Object.create(null));
const names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];
const countedNames = Object.create(null);
for (const name of names) {
const currCount = countedNames[name] ?? 0;
countedNames[name] = currCount + 1;
}
Therefore, if your accumulator is an array or an object and you are copying the array or object on each iteration, you may accidentally introduce quadratic complexity into your code, causing performance to quickly degrade on large data. This has happened in real-world code — see for example Making Tanstack Table 1000x faster with a 1 line change.
Some of the acceptable use cases of reduce()
are given above (most notably, summing an array, promise sequencing, and function piping). There are other cases where better alternatives than reduce()
exist.
- Flattening an array of arrays. Use
flat()
instead.
const flattened = array.reduce((acc, cur) => acc.concat(cur), []);
const flattened = array.flat();
- Grouping objects by a property. Use
Object.groupBy()
instead.
const groups = array.reduce((acc, obj) => {
const key = obj.name;
const curGroup = acc[key] ?? [];
return { ...acc, [key]: [...curGroup, obj] };
}, {});
const groups = Object.groupBy(array, (obj) => obj.name);
- Concatenating arrays contained in an array of objects. Use
flatMap()
instead.
const friends = [
{ name: "Anna", books: ["Bible", "Harry Potter"] },
{ name: "Bob", books: ["War and peace", "Romeo and Juliet"] },
{ name: "Alice", books: ["The Lord of the Rings", "The Shining"] },
];
const allBooks = friends.reduce((acc, cur) => [...acc, ...cur.books], []);
const allBooks = friends.flatMap((person) => person.books);
- Removing duplicate items in an array. Use
Set
and Array.from()
instead.
const uniqArray = array.reduce(
(acc, cur) => (acc.includes(cur) ? acc : [...acc, cur]),
[],
);
const uniqArray = Array.from(new Set(array));
- Eliminating or adding elements in an array. Use
flatMap()
instead.
const roots = array.reduce((acc, cur) => {
if (cur < 0) return acc;
const root = Math.sqrt(cur);
if (Number.isInteger(root)) return [...acc, root, root];
return [...acc, cur];
}, []);
const roots = array.flatMap((val) => {
if (val < 0) return [];
const root = Math.sqrt(val);
if (Number.isInteger(root)) return [root, root];
return [val];
});
If you are only eliminating elements from an array, you also can use filter()
. - Searching for elements or testing if elements satisfy a condition. Use
find()
and findIndex()
, or some()
and every()
instead. These methods have the additional benefit that they return as soon as the result is certain, without iterating the entire array.
const allEven = array.reduce((acc, cur) => acc && cur % 2 === 0, true);
const allEven = array.every((val) => val % 2 === 0);
In cases where reduce()
is the best choice, documentation and semantic variable naming can help mitigate readability drawbacks.