When you have a set of data you want to transform into something else, like an object or a sum, you are reducing.
Method | Returns | One-for-one | Runs for All |
---|---|---|---|
.reduce((accumulator, value, index, array) => *, optionalInitial) |
* |
No | Yes |
The .reduce()
function takes a reducer – a function that collects the end result as it iterates over each entry. Unlike most other array prototype methods, this includes an additional parameter, the accumulator, or the value that is passed from each iteration to the next. This, well, accumulates the new result. An optional initial value is passed to the .reduce()
function as well. If it is not, we receive the first value as the accumulator and save one iteration.
In some examples below we will abbreviate the accumulator
to acc
for brevity, but the meaning is the same.
Code Comparison
const data = [1, 2, 3, 4];
// Older Imperative way - for loop
function sum(data) {
const result = 0;
for (let i = 0; i < data.length; i += 1) {
result += data[i];
}
return result;
}
// Newer Imperative way - for..of loop
function sum(data) {
const result = 0;
for (let value of data) {
result += value;
}
return result;
}
// New Declarative way - .reduce()
const sum = (data) => data
.reduce((acc, value) => acc + value, 0);
// Or, saving that first iteration
const sum = (data) => data.reduce((acc, value) => acc + value);
Use Cases
Sums
The simplest and most common example of .reduce()
is performing arithmetic on an array of numbers.
const data = [1, 2, 3, 4, 5, 6];
const sum = data.reduce((acc, value) => acc + value);
sum; // 21
In this case we didn't provide the optional initial value, so the first array value was passed as the accumulator, but it is roughly equivalent to the following:
const sum = data.reduce((accumulator, value) => {
return accumulator + value;
}, 0);
I used a block body on the arrow function for clarity. It works without it, but the comma separating the initial value can be more confusing to quickly read when we use an expression body:
const sum = data.reduce((acc, value) => acc + value, 0);
Compounding
Another simple arithmetic action is multiplication or compounding. If you've ever needed to calculate "take an extra 20% off the 75% sale!", .reduce()
can be helpful.
const calculatePrice = (discounts, originalPrice) => discounts
.reduce(
(accumulator, discount) => accumulator * (1 - discount),
originalPrice
);
calculatePrice([.5, .3, .1], 1000); // 315
calculatePrice([.75, .2], 150); // 30
Objects, But Carefully!
Creating an object or hash from an array of elements, often called a list or collection, is pretty straightforward. While there are many ways this can happen, be aware that some have serious performance impacts.
// Mutate a new object to add properties.
const result = list.reduce((accumulator, entry) => {
accumulator[entry.key] = entry.value;
return accumulator;
}, {});
// DON'T DO THIS: Substantial performance impact
// Spread to a new object each time.
const result = list.reduce((accumulator, entry) => ({
...accumulator,
[entry.key]: entry.value,
}), {});
There are also alternatives like Object.fromEntries()
that are designed to efficiently turn an array into an object.
Once you exceed a few dozen keys, creating a new object on each iteration can have serious performance impact. We eliminated several hundred milliseconds of processing time in one application by switching to Object.fromEntries()
.
Special Examples
Reduce is somewhat special in that we can also reproduce most of the other array methods with it. It is not as efficient or as clear to do it this way, but we can, so let's see what that looks like.
Filter
const data = [1, 2, 3, 4, 5, 6];
// Filter with reduce
const oddNumbers = data.reduce((accumulator, value) => {
if (isOdd(value)) {
accumulator.push(value);
}
return accumulator;
}, []);
oddNumbers; // [ 1, 3, 5 ]
Some
const data = [1, 2, 3, 4, 5, 6];
const isOdd = (val) => !!(val % 2);
// Some with reduce
const someOdd = data.reduce((accumulator, value) => {
// We have to run each cycle, no matter what
// but we can skip the "work" if we know the answer
return accumulator || isOdd(value);
}, false);
someOdd; // true
Every
// Every with reduce - Starts assuming true
const everyOdd = data.reduce((accumulator, value) => {
// Once it is false, we know "not every"
// so we can "skip" the work.
if (!accumulator) {
return accumulator;
}
return isOdd(value);
}, true);
everyOdd; // false
State Changes
If you use Redux or the useReducer()
hook, the idea is almost the same. Instead of reducing data, we are reducing actions. What we've referred to as the accumulator
is accumulating the state
, and the value
is whatever action
is being performed to update the state. We can take a group of actions and allow each one to make changes.
const updateState = (currentState, setOfActions) => {
return setOfActions.reduce((state, action) => {
// Reducers often use switch for many options
switch (action.type) {
case 'counter/increment':
return {
...state,
counter: state.counter + 1,
};
case 'counter/decrement':
return {
...state,
counter: state.counter - 1,
};
case 'message/set':
return {
...state,
message: action.payload,
};
}
// No match, return current;
return state;
}, currentState);
};
updateState({ counter: 0 }, [{ type: 'counter/increment' }])
// { counter: 1 }
// Perform multiple updates
updateState({ counter: 0 }, [
{ type: 'counter/increment' },
{ type: 'counter/increment' },
{ type: 'counter/decrement' },
{ type: 'message/set', payload: 'Hello!' }
]);
// { counter: 1, message: 'Hello!' }
Flow
Data and actions aren't the only things we can reduce. We can operate on functions as well! This is the basic design of the flow
function used with a functional style of programming.
const flow = (functions) => (input) => functions
.reduce((lastInput, fn) => fn(lastInput), input);
const handshakes = flow([
(x) => x * (x - 1),
(x) => x / 2 ,
]);
handshakes(4); // 6
Taking an initial input, we perform the action and pass on the result to the next function, allowing us to reduce a set of functions into one operation. It may help to think of this as a pipeline.
For this simple example we broke the handshake problem into two steps to demonstrate how flow
works.
Promises
Similar to flow
, but with asynchronous functions we can't directly take the output to pass to the next function. Using promise chains we can wait for the result of one before continuing to the next. This is sometimes called serializing the asynchronous operations.
const asyncFlow = (functions) => (input) => functions.reduce(
// Using .then() passes the last value to the function
(lastPromise, fn) => lastPromise.then(fn),
// Start with resolve to guarantee a promise chain
Promise.resolve(input),
);
While necessary at times, be aware that alternatives like Promise.all()
exist when requests can happen in parallel.
This pattern works like a regular promise chain and assumes each operation will succeed. If any one fails or returns a rejected promise, the rest of the operations will not be executed.
Conclusion
Reduce is perhaps the most flexible of the array methods because it operates on every entry in the array and it has a return value, unlike .forEach()
. There may be more efficient methods for some uses, but if you need to transform an array of data, actions, or functions, .reduce()
is a great place to start.
Top comments (0)