A reducer is a function that takes a collection and for each item in the collection, returns a new state. Most commonly we can use reducers to transform an old state of something to a new state of something. That could be an array to integer, array to array, array of objects representing application state to a new array of objects with the updated application state, anything really.
In most implementations the reduce function relies on 3 key components being available. Firstly is the collection to be reduced, secondly is the reducer function to run for each item in the collection and thirdly is the initial value of the reducer. As an example, in vanilla JavaScript we could do the following:
const numbersToAdd = [1, 2, 3];
function additionReducer(previous, current) {
return previous + current;
}
const result = numbersToAdd.reduce(additionReducer, 0);
console.log(result); // 6
We reduce
our collection passing in a reducer function which receives a previous
and current
value and adds the two together and finally we have the initial value of 0
. What this will do is run the reducer for each iteration of the collection and use the initial value as the initial value of previous
and when we return the result of adding previous
and current
, that value will then become the value of previous
on the next iteration until there are no more items in the collection to iterate and thus, the result is returned.
Tests
describe('reduce', () => {
it('should apply the addition reducer correctly', () => {
const collection = [1, 2, 3];
const reducerFn = (previous, current) => previous + current;
const actual = reduce(collection, reducerFn, 0);
const result = 6;
expect(actual).toStrictEqual(result);
});
it('should return a new array of multiplied values correctly', () => {
const collection = [1, 2, 3];
const reducerFn = (previous, current) => {
previous.push(current * 2);
return previous;
};
const actual = reduce(collection, reducerFn, []);
const result = [2, 4, 6];
expect(actual).toStrictEqual(result);
});
it('should reduce a collection of objects and reshape them via the reducer', () => {
const pokemon = [{
name: "charmander",
type: "fire"
},
{
name: "squirtle",
type: "water"
},
{
name: "bulbasaur",
type: "grass"
}
];
function pokemonReducer(output, current) {
output[current.name] = {
type: current.type
};
return output;
}
const actual = reduce(pokemon, pokemonReducer, {});
const result = {
charmander: {
type: 'fire'
},
squirtle: {
type: 'water'
},
bulbasaur: {
type: 'grass'
}
};
expect(actual).toStrictEqual(result);
});
});
Here we can see 3 reduce
tests which work on similar data but produce values of differing types. That is to say that we have a simple addition reducer just as with the example given in the introduction of this article but also a more complex multiplication reducer which basically acts like a map
function would since it generates a new array of multiplied values. Lastly we see a far more complex reducer which takes a collection of objects and returns a new state representation of each object as a new collection.
Implementation
The native JavaScript implementation of reduce
has the following signature:
arr.reduce(function callback(accumulator, currentValue[, index[, array]]) {
// perform actions and return the next state
}[, initialValue]);
We will aim to reproduce this behaviour with the following implementation:
/**
* @function reduce
* @description A function to a collections values into any other type
* @param {Array} collection - The collection to reduce
* @param {Function} reducerFn - The reducer function to be applied on the last and current value
* @param {*} initialValue - The initial value to apply the reducer to
* @returns {*} The reduced value, this will be the same type as the initialValue parameter
*/
function reduce(collection, reducerFn, initialValue) {
let output = initialValue;
const clone = [...collection];
for (let index = 0; index < clone.length; index++) {
output = reducerFn(output, clone[index], index, clone);
}
return output;
}
The initialValue
will be the default output
of the reduce
function if no items in the collection exist. If items exist in the collection then for each one we will reassign output
to the value of the reducerFn
function. The reducerFn
function takes the same parameters as the native JavaScript implementation since that is our goal to reproduce. These parameters are the accumulator
, currentValue
, index
, array
in the native implementation but in our case they are output
, clone[index]
, index
and clone
.
Note: If you haven't read the previous articles on Array method algorithms in this series, we are cloning the array to avoid mutations on the initial collection that is provided so that that remains intact.
Finally, once our reducerFn
function commits actions against each element and generates a final output
value, we exit the loop and return the output
value.
Using our example of the native implementation near the top of this article, we could do the following to achieve the same results:
const numbersToAdd = [1, 2, 3];
function reduce(collection, reducerFn, initialValue) {
let output = initialValue;
const clone = [...collection];
for (let index = 0; index < clone.length; index++) {
output = reducerFn(output, clone[index], index, clone);
}
return output;
}
function additionReducer(previous, current) {
return previous + current;
}
const result = reduce(numbersToAdd, additionReducer, 0);
console.log(result); // 6
Conclusions
Reducers can be quite a complex topic to discuss but just remember, a reducer merely reduces a collection to a single value. That value could be anything you want it to be but that is all it does. I love using reducers in my day to day work as they can make complex tasks much easier and libraries such as Redux use reducers as a core part of their functionality to do some real heavy lifting. Reducers are also useful for mundane tasks though such as our additionReducer
example and so you can adapt them to many use cases quite easily. In saying this though you do want to scope reducers to highly specific use cases and they should adhere strictly to the Single Responsibility Principle as with any function or method implementation.
Top comments (0)